oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +17 -102
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/{schemas → core/schemas}/device_mapping.json +2 -8
- oscura/{schemas → core/schemas}/packet_format.json +4 -24
- oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -8
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +183 -67
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/tss.py +456 -0
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -0
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +1 -1
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.7.0.dist-info/METADATA +661 -0
- oscura-0.7.0.dist-info/RECORD +591 -0
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -291
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.1.dist-info/METADATA +0 -583
- oscura-0.5.1.dist-info/RECORD +0 -481
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
"""Hardware-in-Loop (HIL) testing framework for real hardware validation.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive HIL testing capabilities for validating protocol
|
|
4
|
+
implementations against real hardware using various interface types.
|
|
5
|
+
|
|
6
|
+
Supports multiple hardware interfaces:
|
|
7
|
+
- Serial ports (UART, RS-232, RS-485) via pyserial
|
|
8
|
+
- SocketCAN for automotive/embedded CAN testing via python-can
|
|
9
|
+
- USB devices via pyusb
|
|
10
|
+
- SPI/I2C via spidev/smbus (Linux only)
|
|
11
|
+
- GPIO control via RPi.GPIO or gpiod
|
|
12
|
+
- Optional oscilloscope integration via PyVISA
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> from oscura.validation.hil_testing import HILTester, HILConfig
|
|
16
|
+
>>> config = HILConfig(
|
|
17
|
+
... interface="serial",
|
|
18
|
+
... port="/dev/ttyUSB0",
|
|
19
|
+
... baud_rate=115200,
|
|
20
|
+
... reset_gpio=17
|
|
21
|
+
... )
|
|
22
|
+
>>> tester = HILTester(config)
|
|
23
|
+
>>> test_cases = [
|
|
24
|
+
... {"name": "ping", "send": b"\\x01\\x02", "expect": b"\\x03\\x04", "timeout": 0.5}
|
|
25
|
+
... ]
|
|
26
|
+
>>> report = tester.run_tests(test_cases)
|
|
27
|
+
>>> print(f"Passed: {report.passed}/{report.total}")
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import time
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from enum import Enum
|
|
35
|
+
from typing import Any, Protocol
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
import can # type: ignore[import-untyped]
|
|
39
|
+
except ImportError:
|
|
40
|
+
can = None # type: ignore[assignment]
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
import usb # type: ignore[import-not-found]
|
|
44
|
+
import usb.core # type: ignore[import-not-found]
|
|
45
|
+
except ImportError:
|
|
46
|
+
# Create module structure for test patching even when pyusb unavailable
|
|
47
|
+
import types
|
|
48
|
+
|
|
49
|
+
usb = types.ModuleType("usb") # type: ignore[assignment]
|
|
50
|
+
usb.core = None # type: ignore[attr-defined]
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
import spidev # type: ignore[import-not-found]
|
|
54
|
+
except ImportError:
|
|
55
|
+
spidev = None # type: ignore[assignment]
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
from smbus2 import SMBus # type: ignore[import-not-found]
|
|
59
|
+
except ImportError:
|
|
60
|
+
SMBus = None # type: ignore[assignment]
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
import RPi.GPIO as GPIO # type: ignore[import-untyped]
|
|
64
|
+
except ImportError:
|
|
65
|
+
try:
|
|
66
|
+
import gpiod # type: ignore[import-not-found]
|
|
67
|
+
|
|
68
|
+
GPIO = None # type: ignore[assignment]
|
|
69
|
+
except ImportError:
|
|
70
|
+
GPIO = None # type: ignore[assignment]
|
|
71
|
+
gpiod = None # type: ignore[assignment]
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
from scapy.all import IP, UDP, Packet, wrpcap # type: ignore[attr-defined]
|
|
75
|
+
except ImportError:
|
|
76
|
+
IP = None # type: ignore[assignment]
|
|
77
|
+
UDP = None # type: ignore[assignment]
|
|
78
|
+
Packet = None # type: ignore[assignment,misc]
|
|
79
|
+
wrpcap = None # type: ignore[assignment]
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
import serial # type: ignore[import-untyped]
|
|
83
|
+
except ImportError:
|
|
84
|
+
serial = None # type: ignore[assignment]
|
|
85
|
+
|
|
86
|
+
from oscura.utils.serial import connect_serial_port
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class CANBusProtocol(Protocol):
|
|
90
|
+
"""Protocol for python-can Bus interface."""
|
|
91
|
+
|
|
92
|
+
def send(self, msg: Any) -> None:
|
|
93
|
+
"""Send a CAN message."""
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
def recv(self, timeout: float | None = None) -> Any:
|
|
97
|
+
"""Receive a CAN message."""
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class InterfaceType(str, Enum):
|
|
102
|
+
"""Supported hardware interface types."""
|
|
103
|
+
|
|
104
|
+
SERIAL = "serial"
|
|
105
|
+
SOCKETCAN = "socketcan"
|
|
106
|
+
USB = "usb"
|
|
107
|
+
SPI = "spi"
|
|
108
|
+
I2C = "i2c"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TestStatus(str, Enum):
|
|
112
|
+
"""Test execution status."""
|
|
113
|
+
|
|
114
|
+
PASSED = "passed"
|
|
115
|
+
FAILED = "failed"
|
|
116
|
+
ERROR = "error"
|
|
117
|
+
TIMEOUT = "timeout"
|
|
118
|
+
SKIPPED = "skipped"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class HILConfig:
|
|
123
|
+
"""Configuration for Hardware-in-Loop testing.
|
|
124
|
+
|
|
125
|
+
Attributes:
|
|
126
|
+
interface: Interface type (serial, socketcan, usb, spi, i2c).
|
|
127
|
+
port: Port identifier (e.g., "/dev/ttyUSB0", "can0", spi device number).
|
|
128
|
+
baud_rate: Baud rate for serial interface (default: 115200).
|
|
129
|
+
timeout: Default timeout in seconds for responses (default: 1.0).
|
|
130
|
+
reset_gpio: GPIO pin number for device reset (optional).
|
|
131
|
+
power_gpio: GPIO pin number for device power control (optional).
|
|
132
|
+
reset_duration: Reset pulse duration in seconds (default: 0.1).
|
|
133
|
+
setup_delay: Delay after setup before testing in seconds (default: 0.5).
|
|
134
|
+
teardown_delay: Delay before teardown in seconds (default: 0.1).
|
|
135
|
+
dry_run: Enable dry-run mode without real hardware (default: False).
|
|
136
|
+
validate_timing: Enable timing validation (default: True).
|
|
137
|
+
capture_pcap: Enable PCAP capture of traffic (default: False).
|
|
138
|
+
pcap_file: Output PCAP file path (default: "hil_capture.pcap").
|
|
139
|
+
oscilloscope_address: VISA address for oscilloscope (optional).
|
|
140
|
+
usb_vendor_id: USB vendor ID (for USB interface).
|
|
141
|
+
usb_product_id: USB product ID (for USB interface).
|
|
142
|
+
spi_bus: SPI bus number (default: 0).
|
|
143
|
+
spi_device: SPI device number (default: 0).
|
|
144
|
+
spi_speed_hz: SPI clock speed in Hz (default: 1000000).
|
|
145
|
+
i2c_bus: I2C bus number (default: 1).
|
|
146
|
+
i2c_address: I2C device address (default: 0x50).
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
interface: InterfaceType | str
|
|
150
|
+
port: str | int
|
|
151
|
+
baud_rate: int = 115200
|
|
152
|
+
timeout: float = 1.0
|
|
153
|
+
reset_gpio: int | None = None
|
|
154
|
+
power_gpio: int | None = None
|
|
155
|
+
reset_duration: float = 0.1
|
|
156
|
+
setup_delay: float = 0.5
|
|
157
|
+
teardown_delay: float = 0.1
|
|
158
|
+
dry_run: bool = False
|
|
159
|
+
validate_timing: bool = True
|
|
160
|
+
capture_pcap: bool = False
|
|
161
|
+
pcap_file: str = "hil_capture.pcap"
|
|
162
|
+
oscilloscope_address: str | None = None
|
|
163
|
+
usb_vendor_id: int | None = None
|
|
164
|
+
usb_product_id: int | None = None
|
|
165
|
+
spi_bus: int = 0
|
|
166
|
+
spi_device: int = 0
|
|
167
|
+
spi_speed_hz: int = 1000000
|
|
168
|
+
i2c_bus: int = 1
|
|
169
|
+
i2c_address: int = 0x50
|
|
170
|
+
|
|
171
|
+
def __post_init__(self) -> None:
|
|
172
|
+
"""Validate configuration after initialization."""
|
|
173
|
+
# Convert string to enum if needed
|
|
174
|
+
if isinstance(self.interface, str):
|
|
175
|
+
try:
|
|
176
|
+
self.interface = InterfaceType(self.interface)
|
|
177
|
+
except ValueError as e:
|
|
178
|
+
raise ValueError(
|
|
179
|
+
f"Invalid interface: {self.interface}. "
|
|
180
|
+
f"Must be one of: {', '.join(t.value for t in InterfaceType)}"
|
|
181
|
+
) from e
|
|
182
|
+
|
|
183
|
+
if self.timeout <= 0:
|
|
184
|
+
raise ValueError(f"timeout must be positive, got {self.timeout}")
|
|
185
|
+
if self.baud_rate <= 0:
|
|
186
|
+
raise ValueError(f"baud_rate must be positive, got {self.baud_rate}")
|
|
187
|
+
if self.reset_duration < 0:
|
|
188
|
+
raise ValueError(f"reset_duration must be non-negative, got {self.reset_duration}")
|
|
189
|
+
if self.setup_delay < 0:
|
|
190
|
+
raise ValueError(f"setup_delay must be non-negative, got {self.setup_delay}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass
|
|
194
|
+
class HILTestResult:
|
|
195
|
+
"""Result from a single HIL test case.
|
|
196
|
+
|
|
197
|
+
Attributes:
|
|
198
|
+
test_name: Name of the test case.
|
|
199
|
+
status: Test execution status (passed, failed, error, timeout, skipped).
|
|
200
|
+
sent_data: Data sent to hardware (as hex string).
|
|
201
|
+
received_data: Data received from hardware (as hex string, None if timeout).
|
|
202
|
+
expected_data: Expected response data (as hex string, None if not specified).
|
|
203
|
+
latency: Response latency in seconds (None if timeout).
|
|
204
|
+
error: Error message if status is ERROR (None otherwise).
|
|
205
|
+
timestamp: Test execution timestamp.
|
|
206
|
+
timing_valid: Whether timing was within tolerance (None if not validated).
|
|
207
|
+
bit_errors: Number of bit errors detected (0 if perfect match).
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
test_name: str
|
|
211
|
+
status: TestStatus
|
|
212
|
+
sent_data: str
|
|
213
|
+
received_data: str | None
|
|
214
|
+
expected_data: str | None
|
|
215
|
+
latency: float | None
|
|
216
|
+
error: str | None = None
|
|
217
|
+
timestamp: float = field(default_factory=time.time)
|
|
218
|
+
timing_valid: bool | None = None
|
|
219
|
+
bit_errors: int = 0
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def passed(self) -> bool:
|
|
223
|
+
"""Check if test passed."""
|
|
224
|
+
return self.status == TestStatus.PASSED
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass
|
|
228
|
+
class HILTestReport:
|
|
229
|
+
"""Comprehensive report from HIL test execution.
|
|
230
|
+
|
|
231
|
+
Attributes:
|
|
232
|
+
test_results: List of individual test results.
|
|
233
|
+
total: Total number of tests executed.
|
|
234
|
+
passed: Number of tests that passed.
|
|
235
|
+
failed: Number of tests that failed.
|
|
236
|
+
errors: Number of tests with errors.
|
|
237
|
+
timeouts: Number of tests that timed out.
|
|
238
|
+
skipped: Number of tests that were skipped.
|
|
239
|
+
hardware_info: Hardware configuration information.
|
|
240
|
+
timing_statistics: Timing statistics (min/max/avg latency in seconds).
|
|
241
|
+
start_time: Test suite start timestamp.
|
|
242
|
+
end_time: Test suite end timestamp.
|
|
243
|
+
duration: Total execution duration in seconds.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
test_results: list[HILTestResult]
|
|
247
|
+
total: int
|
|
248
|
+
passed: int
|
|
249
|
+
failed: int
|
|
250
|
+
errors: int
|
|
251
|
+
timeouts: int
|
|
252
|
+
skipped: int
|
|
253
|
+
hardware_info: dict[str, Any]
|
|
254
|
+
timing_statistics: dict[str, float]
|
|
255
|
+
start_time: float
|
|
256
|
+
end_time: float
|
|
257
|
+
duration: float
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def success_rate(self) -> float:
|
|
261
|
+
"""Calculate overall success rate.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Success rate as fraction (0.0-1.0).
|
|
265
|
+
"""
|
|
266
|
+
return self.passed / self.total if self.total > 0 else 0.0
|
|
267
|
+
|
|
268
|
+
def to_dict(self) -> dict[str, Any]:
|
|
269
|
+
"""Export report to dictionary for JSON serialization.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Dictionary with complete test report data.
|
|
273
|
+
"""
|
|
274
|
+
return {
|
|
275
|
+
"test_results": [
|
|
276
|
+
{
|
|
277
|
+
"test_name": r.test_name,
|
|
278
|
+
"status": r.status.value,
|
|
279
|
+
"sent_data": r.sent_data,
|
|
280
|
+
"received_data": r.received_data,
|
|
281
|
+
"expected_data": r.expected_data,
|
|
282
|
+
"latency": r.latency,
|
|
283
|
+
"error": r.error,
|
|
284
|
+
"timestamp": r.timestamp,
|
|
285
|
+
"timing_valid": r.timing_valid,
|
|
286
|
+
"bit_errors": r.bit_errors,
|
|
287
|
+
}
|
|
288
|
+
for r in self.test_results
|
|
289
|
+
],
|
|
290
|
+
"summary": {
|
|
291
|
+
"total": self.total,
|
|
292
|
+
"passed": self.passed,
|
|
293
|
+
"failed": self.failed,
|
|
294
|
+
"errors": self.errors,
|
|
295
|
+
"timeouts": self.timeouts,
|
|
296
|
+
"skipped": self.skipped,
|
|
297
|
+
"success_rate": self.success_rate,
|
|
298
|
+
},
|
|
299
|
+
"hardware_info": self.hardware_info,
|
|
300
|
+
"timing_statistics": self.timing_statistics,
|
|
301
|
+
"start_time": self.start_time,
|
|
302
|
+
"end_time": self.end_time,
|
|
303
|
+
"duration": self.duration,
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class HILTester:
|
|
308
|
+
"""Hardware-in-Loop testing framework.
|
|
309
|
+
|
|
310
|
+
Automates hardware validation testing by sending test vectors to real hardware
|
|
311
|
+
and validating responses against expected behavior. Supports multiple interface
|
|
312
|
+
types with automatic setup/teardown.
|
|
313
|
+
|
|
314
|
+
Example:
|
|
315
|
+
>>> config = HILConfig(interface="serial", port="/dev/ttyUSB0")
|
|
316
|
+
>>> tester = HILTester(config)
|
|
317
|
+
>>> tester.setup()
|
|
318
|
+
>>> test = {"name": "echo", "send": b"\\x01", "expect": b"\\x01"}
|
|
319
|
+
>>> result = tester.run_test(test)
|
|
320
|
+
>>> tester.teardown()
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
def __init__(self, config: HILConfig) -> None:
|
|
324
|
+
"""Initialize HIL tester with configuration.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
config: HIL testing configuration.
|
|
328
|
+
"""
|
|
329
|
+
self.config = config
|
|
330
|
+
self._connection: Any = None
|
|
331
|
+
self._gpio_controller: Any = None
|
|
332
|
+
self._is_setup = False
|
|
333
|
+
self._pcap_packets: list[tuple[float, bytes, bytes | None]] = []
|
|
334
|
+
|
|
335
|
+
def __enter__(self) -> HILTester:
|
|
336
|
+
"""Context manager entry - setup hardware."""
|
|
337
|
+
self.setup()
|
|
338
|
+
return self
|
|
339
|
+
|
|
340
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
341
|
+
"""Context manager exit - teardown hardware."""
|
|
342
|
+
self.teardown()
|
|
343
|
+
|
|
344
|
+
def setup(self) -> None:
|
|
345
|
+
"""Setup hardware connection and initialize device.
|
|
346
|
+
|
|
347
|
+
Performs:
|
|
348
|
+
1. GPIO initialization (if configured)
|
|
349
|
+
2. Power on device (if power GPIO configured)
|
|
350
|
+
3. Reset device (if reset GPIO configured)
|
|
351
|
+
4. Initialize interface connection
|
|
352
|
+
5. Wait for device stability
|
|
353
|
+
|
|
354
|
+
Raises:
|
|
355
|
+
ImportError: If required library is not installed.
|
|
356
|
+
OSError: If hardware connection fails.
|
|
357
|
+
RuntimeError: If already setup.
|
|
358
|
+
"""
|
|
359
|
+
if self._is_setup:
|
|
360
|
+
raise RuntimeError("Already setup. Call teardown() first.")
|
|
361
|
+
|
|
362
|
+
# Initialize GPIO if needed
|
|
363
|
+
if self.config.reset_gpio is not None or self.config.power_gpio is not None:
|
|
364
|
+
self._setup_gpio()
|
|
365
|
+
|
|
366
|
+
# Power on device if configured
|
|
367
|
+
if self.config.power_gpio is not None:
|
|
368
|
+
self._power_on()
|
|
369
|
+
|
|
370
|
+
# Reset device if configured
|
|
371
|
+
if self.config.reset_gpio is not None:
|
|
372
|
+
self._reset_device()
|
|
373
|
+
|
|
374
|
+
# Connect to hardware interface
|
|
375
|
+
if not self.config.dry_run:
|
|
376
|
+
self._connect()
|
|
377
|
+
|
|
378
|
+
# Wait for device to stabilize
|
|
379
|
+
if self.config.setup_delay > 0:
|
|
380
|
+
time.sleep(self.config.setup_delay)
|
|
381
|
+
|
|
382
|
+
self._is_setup = True
|
|
383
|
+
|
|
384
|
+
def teardown(self) -> None:
|
|
385
|
+
"""Teardown hardware connection and cleanup resources.
|
|
386
|
+
|
|
387
|
+
Performs:
|
|
388
|
+
1. Wait for teardown delay
|
|
389
|
+
2. Close interface connection
|
|
390
|
+
3. Power off device (if power GPIO configured)
|
|
391
|
+
4. Cleanup GPIO resources
|
|
392
|
+
5. Export PCAP if capture enabled
|
|
393
|
+
"""
|
|
394
|
+
if not self._is_setup:
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
# Wait before teardown
|
|
398
|
+
if self.config.teardown_delay > 0:
|
|
399
|
+
time.sleep(self.config.teardown_delay)
|
|
400
|
+
|
|
401
|
+
# Close connection
|
|
402
|
+
if self._connection is not None:
|
|
403
|
+
try:
|
|
404
|
+
if hasattr(self._connection, "close"):
|
|
405
|
+
self._connection.close()
|
|
406
|
+
elif hasattr(self._connection, "shutdown"):
|
|
407
|
+
self._connection.shutdown()
|
|
408
|
+
except Exception:
|
|
409
|
+
pass # Best effort cleanup
|
|
410
|
+
finally:
|
|
411
|
+
self._connection = None
|
|
412
|
+
|
|
413
|
+
# Power off device if configured
|
|
414
|
+
if self.config.power_gpio is not None and self._gpio_controller is not None:
|
|
415
|
+
self._power_off()
|
|
416
|
+
|
|
417
|
+
# Cleanup GPIO
|
|
418
|
+
if self._gpio_controller is not None:
|
|
419
|
+
try:
|
|
420
|
+
if hasattr(self._gpio_controller, "cleanup"):
|
|
421
|
+
self._gpio_controller.cleanup()
|
|
422
|
+
except Exception:
|
|
423
|
+
pass
|
|
424
|
+
finally:
|
|
425
|
+
self._gpio_controller = None
|
|
426
|
+
|
|
427
|
+
# Export PCAP if enabled
|
|
428
|
+
if self.config.capture_pcap and self._pcap_packets:
|
|
429
|
+
self._export_pcap()
|
|
430
|
+
|
|
431
|
+
self._is_setup = False
|
|
432
|
+
|
|
433
|
+
def run_test(
|
|
434
|
+
self,
|
|
435
|
+
test_case: dict[str, Any],
|
|
436
|
+
) -> HILTestResult:
|
|
437
|
+
"""Execute a single test case.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
test_case: Test case dictionary with keys:
|
|
441
|
+
- name (str): Test case name
|
|
442
|
+
- send (bytes): Data to send to hardware
|
|
443
|
+
- expect (bytes, optional): Expected response data
|
|
444
|
+
- timeout (float, optional): Override default timeout
|
|
445
|
+
- max_latency (float, optional): Maximum acceptable latency
|
|
446
|
+
- min_latency (float, optional): Minimum acceptable latency
|
|
447
|
+
- skip (bool, optional): Skip this test
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
Test result with status, timing, and validation info.
|
|
451
|
+
|
|
452
|
+
Raises:
|
|
453
|
+
RuntimeError: If not setup.
|
|
454
|
+
|
|
455
|
+
Example:
|
|
456
|
+
>>> tester = HILTester(HILConfig("serial", "/dev/ttyUSB0"))
|
|
457
|
+
>>> tester.setup()
|
|
458
|
+
>>> result = tester.run_test({
|
|
459
|
+
... "name": "echo_test",
|
|
460
|
+
... "send": b"\\x01\\x02",
|
|
461
|
+
... "expect": b"\\x01\\x02",
|
|
462
|
+
... "timeout": 0.5
|
|
463
|
+
... })
|
|
464
|
+
>>> tester.teardown()
|
|
465
|
+
"""
|
|
466
|
+
if not self._is_setup:
|
|
467
|
+
raise RuntimeError("Not setup. Call setup() first.")
|
|
468
|
+
|
|
469
|
+
test_params = self._extract_test_parameters(test_case)
|
|
470
|
+
if test_params["skip"]:
|
|
471
|
+
return self._create_skipped_result(test_params)
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
return self._execute_test_case(test_params)
|
|
475
|
+
except Exception as e:
|
|
476
|
+
return self._create_error_result(test_params, e)
|
|
477
|
+
|
|
478
|
+
def _extract_test_parameters(self, test_case: dict[str, Any]) -> dict[str, Any]:
|
|
479
|
+
"""Extract test parameters from test case dictionary."""
|
|
480
|
+
return {
|
|
481
|
+
"test_name": test_case.get("name", "unnamed_test"),
|
|
482
|
+
"send_data": test_case.get("send", b""),
|
|
483
|
+
"expect_data": test_case.get("expect"),
|
|
484
|
+
"timeout": test_case.get("timeout", self.config.timeout),
|
|
485
|
+
"max_latency": test_case.get("max_latency"),
|
|
486
|
+
"min_latency": test_case.get("min_latency"),
|
|
487
|
+
"skip": test_case.get("skip", False),
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
def _create_skipped_result(self, test_params: dict[str, Any]) -> HILTestResult:
|
|
491
|
+
"""Create result for skipped test."""
|
|
492
|
+
return HILTestResult(
|
|
493
|
+
test_name=test_params["test_name"],
|
|
494
|
+
status=TestStatus.SKIPPED,
|
|
495
|
+
sent_data=test_params["send_data"].hex(),
|
|
496
|
+
received_data=None,
|
|
497
|
+
expected_data=test_params["expect_data"].hex() if test_params["expect_data"] else None,
|
|
498
|
+
latency=None,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
def _execute_test_case(self, test_params: dict[str, Any]) -> HILTestResult:
|
|
502
|
+
"""Execute test case and return result."""
|
|
503
|
+
start_time = time.time()
|
|
504
|
+
response = self._send_receive(test_params["send_data"], test_params["timeout"])
|
|
505
|
+
end_time = time.time()
|
|
506
|
+
latency = end_time - start_time
|
|
507
|
+
|
|
508
|
+
if self.config.capture_pcap:
|
|
509
|
+
self._pcap_packets.append((start_time, test_params["send_data"], response))
|
|
510
|
+
|
|
511
|
+
if response is None:
|
|
512
|
+
return self._create_timeout_result(test_params)
|
|
513
|
+
|
|
514
|
+
status, bit_errors = self._evaluate_response(response, test_params["expect_data"])
|
|
515
|
+
timing_valid = self._validate_timing(
|
|
516
|
+
latency, test_params["max_latency"], test_params["min_latency"], status
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
if timing_valid is False and status == TestStatus.PASSED:
|
|
520
|
+
status = TestStatus.FAILED
|
|
521
|
+
|
|
522
|
+
return HILTestResult(
|
|
523
|
+
test_name=test_params["test_name"],
|
|
524
|
+
status=status,
|
|
525
|
+
sent_data=test_params["send_data"].hex(),
|
|
526
|
+
received_data=response.hex(),
|
|
527
|
+
expected_data=test_params["expect_data"].hex() if test_params["expect_data"] else None,
|
|
528
|
+
latency=latency,
|
|
529
|
+
timing_valid=timing_valid,
|
|
530
|
+
bit_errors=bit_errors,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
def _create_timeout_result(self, test_params: dict[str, Any]) -> HILTestResult:
|
|
534
|
+
"""Create result for timed-out test."""
|
|
535
|
+
return HILTestResult(
|
|
536
|
+
test_name=test_params["test_name"],
|
|
537
|
+
status=TestStatus.TIMEOUT,
|
|
538
|
+
sent_data=test_params["send_data"].hex(),
|
|
539
|
+
received_data=None,
|
|
540
|
+
expected_data=test_params["expect_data"].hex() if test_params["expect_data"] else None,
|
|
541
|
+
latency=None,
|
|
542
|
+
timing_valid=None,
|
|
543
|
+
bit_errors=0,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
def _evaluate_response(
|
|
547
|
+
self, response: bytes, expect_data: bytes | None
|
|
548
|
+
) -> tuple[TestStatus, int]:
|
|
549
|
+
"""Evaluate response against expected data."""
|
|
550
|
+
if expect_data is None:
|
|
551
|
+
return TestStatus.PASSED, 0
|
|
552
|
+
if response == expect_data:
|
|
553
|
+
return TestStatus.PASSED, 0
|
|
554
|
+
return TestStatus.FAILED, self._count_bit_errors(response, expect_data)
|
|
555
|
+
|
|
556
|
+
def _validate_timing(
|
|
557
|
+
self,
|
|
558
|
+
latency: float,
|
|
559
|
+
max_latency: float | None,
|
|
560
|
+
min_latency: float | None,
|
|
561
|
+
status: TestStatus,
|
|
562
|
+
) -> bool | None:
|
|
563
|
+
"""Validate timing constraints."""
|
|
564
|
+
if not self.config.validate_timing or (not max_latency and not min_latency):
|
|
565
|
+
return None
|
|
566
|
+
timing_valid = True
|
|
567
|
+
if max_latency and latency > max_latency:
|
|
568
|
+
timing_valid = False
|
|
569
|
+
if min_latency and latency < min_latency:
|
|
570
|
+
timing_valid = False
|
|
571
|
+
return timing_valid
|
|
572
|
+
|
|
573
|
+
def _create_error_result(self, test_params: dict[str, Any], error: Exception) -> HILTestResult:
|
|
574
|
+
"""Create result for test with error."""
|
|
575
|
+
return HILTestResult(
|
|
576
|
+
test_name=test_params["test_name"],
|
|
577
|
+
status=TestStatus.ERROR,
|
|
578
|
+
sent_data=test_params["send_data"].hex(),
|
|
579
|
+
received_data=None,
|
|
580
|
+
expected_data=test_params["expect_data"].hex() if test_params["expect_data"] else None,
|
|
581
|
+
latency=None,
|
|
582
|
+
error=f"{type(error).__name__}: {error}",
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
def run_tests(self, test_cases: list[dict[str, Any]]) -> HILTestReport:
|
|
586
|
+
"""Execute a suite of test cases.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
test_cases: List of test case dictionaries (see run_test for format).
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
Comprehensive test report with statistics and all results.
|
|
593
|
+
|
|
594
|
+
Example:
|
|
595
|
+
>>> config = HILConfig(interface="serial", port="/dev/ttyUSB0")
|
|
596
|
+
>>> tester = HILTester(config)
|
|
597
|
+
>>> tests = [
|
|
598
|
+
... {"name": "test1", "send": b"\\x01", "expect": b"\\x02"},
|
|
599
|
+
... {"name": "test2", "send": b"\\x03", "expect": b"\\x04"},
|
|
600
|
+
... ]
|
|
601
|
+
>>> with tester:
|
|
602
|
+
... report = tester.run_tests(tests)
|
|
603
|
+
>>> print(f"Success rate: {report.success_rate:.1%}")
|
|
604
|
+
"""
|
|
605
|
+
start_time = time.time()
|
|
606
|
+
results: list[HILTestResult] = []
|
|
607
|
+
latencies: list[float] = []
|
|
608
|
+
|
|
609
|
+
for test_case in test_cases:
|
|
610
|
+
result = self.run_test(test_case)
|
|
611
|
+
results.append(result)
|
|
612
|
+
if result.latency is not None:
|
|
613
|
+
latencies.append(result.latency)
|
|
614
|
+
|
|
615
|
+
end_time = time.time()
|
|
616
|
+
|
|
617
|
+
# Calculate statistics
|
|
618
|
+
passed = sum(1 for r in results if r.status == TestStatus.PASSED)
|
|
619
|
+
failed = sum(1 for r in results if r.status == TestStatus.FAILED)
|
|
620
|
+
errors = sum(1 for r in results if r.status == TestStatus.ERROR)
|
|
621
|
+
timeouts = sum(1 for r in results if r.status == TestStatus.TIMEOUT)
|
|
622
|
+
skipped = sum(1 for r in results if r.status == TestStatus.SKIPPED)
|
|
623
|
+
|
|
624
|
+
timing_stats = {}
|
|
625
|
+
if latencies:
|
|
626
|
+
timing_stats = {
|
|
627
|
+
"min_latency": min(latencies),
|
|
628
|
+
"max_latency": max(latencies),
|
|
629
|
+
"avg_latency": sum(latencies) / len(latencies),
|
|
630
|
+
"total_samples": len(latencies),
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
# Interface is always InterfaceType after __post_init__
|
|
634
|
+
interface_value = (
|
|
635
|
+
self.config.interface.value
|
|
636
|
+
if isinstance(self.config.interface, InterfaceType)
|
|
637
|
+
else self.config.interface
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
hardware_info = {
|
|
641
|
+
"interface": interface_value,
|
|
642
|
+
"port": str(self.config.port),
|
|
643
|
+
"baud_rate": self.config.baud_rate,
|
|
644
|
+
"timeout": self.config.timeout,
|
|
645
|
+
"dry_run": self.config.dry_run,
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return HILTestReport(
|
|
649
|
+
test_results=results,
|
|
650
|
+
total=len(results),
|
|
651
|
+
passed=passed,
|
|
652
|
+
failed=failed,
|
|
653
|
+
errors=errors,
|
|
654
|
+
timeouts=timeouts,
|
|
655
|
+
skipped=skipped,
|
|
656
|
+
hardware_info=hardware_info,
|
|
657
|
+
timing_statistics=timing_stats,
|
|
658
|
+
start_time=start_time,
|
|
659
|
+
end_time=end_time,
|
|
660
|
+
duration=end_time - start_time,
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
def _setup_gpio(self) -> None:
|
|
664
|
+
"""Initialize GPIO controller.
|
|
665
|
+
|
|
666
|
+
Tries to use gpiod first (modern Linux), falls back to RPi.GPIO.
|
|
667
|
+
|
|
668
|
+
Raises:
|
|
669
|
+
ImportError: If no GPIO library is available.
|
|
670
|
+
"""
|
|
671
|
+
if "gpiod" in globals() and gpiod is not None:
|
|
672
|
+
# Use libgpiod for modern Linux systems
|
|
673
|
+
self._gpio_controller = gpiod
|
|
674
|
+
elif GPIO is not None:
|
|
675
|
+
GPIO.setmode(GPIO.BCM)
|
|
676
|
+
GPIO.setwarnings(False)
|
|
677
|
+
self._gpio_controller = GPIO
|
|
678
|
+
|
|
679
|
+
# Setup pins as outputs
|
|
680
|
+
if self.config.reset_gpio is not None:
|
|
681
|
+
GPIO.setup(self.config.reset_gpio, GPIO.OUT, initial=GPIO.HIGH)
|
|
682
|
+
if self.config.power_gpio is not None:
|
|
683
|
+
GPIO.setup(self.config.power_gpio, GPIO.OUT, initial=GPIO.LOW)
|
|
684
|
+
else:
|
|
685
|
+
raise ImportError(
|
|
686
|
+
"No GPIO library available. Install gpiod or RPi.GPIO: "
|
|
687
|
+
"pip install gpiod # or pip install RPi.GPIO"
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
def _power_on(self) -> None:
|
|
691
|
+
"""Power on device via GPIO."""
|
|
692
|
+
if self._gpio_controller is None or self.config.power_gpio is None:
|
|
693
|
+
return
|
|
694
|
+
|
|
695
|
+
# Assuming active-high power control
|
|
696
|
+
if hasattr(self._gpio_controller, "output"):
|
|
697
|
+
# RPi.GPIO
|
|
698
|
+
self._gpio_controller.output(self.config.power_gpio, True)
|
|
699
|
+
# Add gpiod support if needed
|
|
700
|
+
|
|
701
|
+
def _power_off(self) -> None:
|
|
702
|
+
"""Power off device via GPIO."""
|
|
703
|
+
if self._gpio_controller is None or self.config.power_gpio is None:
|
|
704
|
+
return
|
|
705
|
+
|
|
706
|
+
if hasattr(self._gpio_controller, "output"):
|
|
707
|
+
# RPi.GPIO
|
|
708
|
+
self._gpio_controller.output(self.config.power_gpio, False)
|
|
709
|
+
|
|
710
|
+
def _reset_device(self) -> None:
|
|
711
|
+
"""Reset device via GPIO pulse."""
|
|
712
|
+
if self._gpio_controller is None or self.config.reset_gpio is None:
|
|
713
|
+
return
|
|
714
|
+
|
|
715
|
+
# Assuming active-low reset (pulse low to reset)
|
|
716
|
+
if hasattr(self._gpio_controller, "output"):
|
|
717
|
+
# RPi.GPIO
|
|
718
|
+
self._gpio_controller.output(self.config.reset_gpio, False)
|
|
719
|
+
time.sleep(self.config.reset_duration)
|
|
720
|
+
self._gpio_controller.output(self.config.reset_gpio, True)
|
|
721
|
+
|
|
722
|
+
def _connect(self) -> None:
|
|
723
|
+
"""Connect to hardware interface.
|
|
724
|
+
|
|
725
|
+
Raises:
|
|
726
|
+
ImportError: If required library is not installed.
|
|
727
|
+
OSError: If connection fails.
|
|
728
|
+
"""
|
|
729
|
+
if self.config.interface == InterfaceType.SERIAL:
|
|
730
|
+
self._connect_serial()
|
|
731
|
+
elif self.config.interface == InterfaceType.SOCKETCAN:
|
|
732
|
+
self._connect_socketcan()
|
|
733
|
+
elif self.config.interface == InterfaceType.USB:
|
|
734
|
+
self._connect_usb()
|
|
735
|
+
elif self.config.interface == InterfaceType.SPI:
|
|
736
|
+
self._connect_spi()
|
|
737
|
+
elif self.config.interface == InterfaceType.I2C:
|
|
738
|
+
self._connect_i2c()
|
|
739
|
+
|
|
740
|
+
def _connect_serial(self) -> None:
|
|
741
|
+
"""Connect to serial port.
|
|
742
|
+
|
|
743
|
+
Raises:
|
|
744
|
+
ImportError: If pyserial is not installed.
|
|
745
|
+
OSError: If serial port cannot be opened.
|
|
746
|
+
"""
|
|
747
|
+
self._connection = connect_serial_port(
|
|
748
|
+
port=str(self.config.port),
|
|
749
|
+
baud_rate=self.config.baud_rate,
|
|
750
|
+
timeout=self.config.timeout,
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
def _connect_socketcan(self) -> None:
|
|
754
|
+
"""Connect to SocketCAN interface.
|
|
755
|
+
|
|
756
|
+
Raises:
|
|
757
|
+
ImportError: If python-can is not installed.
|
|
758
|
+
OSError: If CAN interface cannot be opened.
|
|
759
|
+
"""
|
|
760
|
+
if can is None:
|
|
761
|
+
raise ImportError(
|
|
762
|
+
"python-can is required for SocketCAN. Install with: pip install python-can"
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
if not isinstance(self.config.port, str):
|
|
766
|
+
raise ValueError(f"CAN interface must be string, got {type(self.config.port)}")
|
|
767
|
+
|
|
768
|
+
self._connection = can.interface.Bus(
|
|
769
|
+
channel=self.config.port, interface="socketcan", receive_own_messages=False
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
def _connect_usb(self) -> None:
|
|
773
|
+
"""Connect to USB device.
|
|
774
|
+
|
|
775
|
+
Raises:
|
|
776
|
+
ImportError: If pyusb is not installed.
|
|
777
|
+
OSError: If USB device not found or cannot be opened.
|
|
778
|
+
"""
|
|
779
|
+
if getattr(usb, "core", None) is None:
|
|
780
|
+
raise ImportError(
|
|
781
|
+
"pyusb is required for USB interface. Install with: pip install pyusb"
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
if self.config.usb_vendor_id is None or self.config.usb_product_id is None:
|
|
785
|
+
raise ValueError("usb_vendor_id and usb_product_id must be set for USB interface")
|
|
786
|
+
|
|
787
|
+
dev = usb.core.find(
|
|
788
|
+
idVendor=self.config.usb_vendor_id, idProduct=self.config.usb_product_id
|
|
789
|
+
)
|
|
790
|
+
if dev is None:
|
|
791
|
+
raise OSError(
|
|
792
|
+
f"USB device not found: {self.config.usb_vendor_id:04x}:"
|
|
793
|
+
f"{self.config.usb_product_id:04x}"
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
self._connection = dev
|
|
797
|
+
|
|
798
|
+
def _connect_spi(self) -> None:
|
|
799
|
+
"""Connect to SPI device.
|
|
800
|
+
|
|
801
|
+
Raises:
|
|
802
|
+
ImportError: If spidev is not installed.
|
|
803
|
+
OSError: If SPI device cannot be opened.
|
|
804
|
+
"""
|
|
805
|
+
if spidev is None:
|
|
806
|
+
raise ImportError(
|
|
807
|
+
"spidev is required for SPI interface. Install with: pip install spidev"
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
spi = spidev.SpiDev()
|
|
811
|
+
spi.open(self.config.spi_bus, self.config.spi_device)
|
|
812
|
+
spi.max_speed_hz = self.config.spi_speed_hz
|
|
813
|
+
self._connection = spi
|
|
814
|
+
|
|
815
|
+
def _connect_i2c(self) -> None:
|
|
816
|
+
"""Connect to I2C device.
|
|
817
|
+
|
|
818
|
+
Raises:
|
|
819
|
+
ImportError: If smbus2 is not installed.
|
|
820
|
+
OSError: If I2C device cannot be opened.
|
|
821
|
+
"""
|
|
822
|
+
if SMBus is None:
|
|
823
|
+
raise ImportError(
|
|
824
|
+
"smbus2 is required for I2C interface. Install with: pip install smbus2"
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
self._connection = SMBus(self.config.i2c_bus)
|
|
828
|
+
|
|
829
|
+
def _send_receive(self, data: bytes, timeout: float) -> bytes | None:
|
|
830
|
+
"""Send data and receive response.
|
|
831
|
+
|
|
832
|
+
Args:
|
|
833
|
+
data: Data to send.
|
|
834
|
+
timeout: Timeout in seconds.
|
|
835
|
+
|
|
836
|
+
Returns:
|
|
837
|
+
Response data, or None if timeout.
|
|
838
|
+
|
|
839
|
+
Raises:
|
|
840
|
+
OSError: If send/receive fails.
|
|
841
|
+
"""
|
|
842
|
+
if self.config.dry_run:
|
|
843
|
+
# In dry-run mode, echo the data back
|
|
844
|
+
time.sleep(0.001) # Simulate minimal latency
|
|
845
|
+
return data
|
|
846
|
+
|
|
847
|
+
if self.config.interface == InterfaceType.SERIAL:
|
|
848
|
+
return self._send_receive_serial(data, timeout)
|
|
849
|
+
elif self.config.interface == InterfaceType.SOCKETCAN:
|
|
850
|
+
return self._send_receive_socketcan(data, timeout)
|
|
851
|
+
elif self.config.interface == InterfaceType.USB:
|
|
852
|
+
return self._send_receive_usb(data, timeout)
|
|
853
|
+
elif self.config.interface == InterfaceType.SPI:
|
|
854
|
+
return self._send_receive_spi(data)
|
|
855
|
+
elif self.config.interface == InterfaceType.I2C:
|
|
856
|
+
return self._send_receive_i2c(data)
|
|
857
|
+
|
|
858
|
+
return None
|
|
859
|
+
|
|
860
|
+
def _send_receive_serial(self, data: bytes, timeout: float) -> bytes | None:
|
|
861
|
+
"""Send/receive via serial port."""
|
|
862
|
+
ser: serial.Serial = self._connection
|
|
863
|
+
original_timeout = ser.timeout
|
|
864
|
+
ser.timeout = timeout
|
|
865
|
+
ser.reset_input_buffer()
|
|
866
|
+
ser.write(data)
|
|
867
|
+
ser.flush()
|
|
868
|
+
|
|
869
|
+
# Read response
|
|
870
|
+
response = ser.read(1024)
|
|
871
|
+
ser.timeout = original_timeout
|
|
872
|
+
return response if response else None
|
|
873
|
+
|
|
874
|
+
def _send_receive_socketcan(self, data: bytes, timeout: float) -> bytes | None:
|
|
875
|
+
"""Send/receive via SocketCAN."""
|
|
876
|
+
bus: CANBusProtocol = self._connection
|
|
877
|
+
msg = can.Message(arbitration_id=0x123, data=data, is_extended_id=False)
|
|
878
|
+
bus.send(msg)
|
|
879
|
+
|
|
880
|
+
response_msg = bus.recv(timeout=timeout)
|
|
881
|
+
return bytes(response_msg.data) if response_msg else None
|
|
882
|
+
|
|
883
|
+
def _send_receive_usb(self, data: bytes, timeout: float) -> bytes | None:
|
|
884
|
+
"""Send/receive via USB bulk transfer."""
|
|
885
|
+
dev = self._connection
|
|
886
|
+
endpoint_out = 0x01 # Typically endpoint 1 OUT
|
|
887
|
+
endpoint_in = 0x81 # Typically endpoint 1 IN
|
|
888
|
+
|
|
889
|
+
# Send data
|
|
890
|
+
dev.write(endpoint_out, data, int(timeout * 1000))
|
|
891
|
+
|
|
892
|
+
# Receive response
|
|
893
|
+
try:
|
|
894
|
+
response = dev.read(endpoint_in, 1024, int(timeout * 1000))
|
|
895
|
+
return bytes(response) if response else None
|
|
896
|
+
except Exception:
|
|
897
|
+
return None
|
|
898
|
+
|
|
899
|
+
def _send_receive_spi(self, data: bytes) -> bytes | None:
|
|
900
|
+
"""Send/receive via SPI (full-duplex)."""
|
|
901
|
+
spi = self._connection
|
|
902
|
+
response = spi.xfer2(list(data))
|
|
903
|
+
return bytes(response)
|
|
904
|
+
|
|
905
|
+
def _send_receive_i2c(self, data: bytes) -> bytes | None:
|
|
906
|
+
"""Send/receive via I2C."""
|
|
907
|
+
bus = self._connection
|
|
908
|
+
# Write data
|
|
909
|
+
for byte in data:
|
|
910
|
+
bus.write_byte(self.config.i2c_address, byte)
|
|
911
|
+
|
|
912
|
+
# Read response (assume same length as sent)
|
|
913
|
+
response = []
|
|
914
|
+
for _ in range(len(data)):
|
|
915
|
+
response.append(bus.read_byte(self.config.i2c_address))
|
|
916
|
+
|
|
917
|
+
return bytes(response)
|
|
918
|
+
|
|
919
|
+
def _count_bit_errors(self, received: bytes, expected: bytes) -> int:
|
|
920
|
+
"""Count number of bit errors between received and expected data.
|
|
921
|
+
|
|
922
|
+
Args:
|
|
923
|
+
received: Received data.
|
|
924
|
+
expected: Expected data.
|
|
925
|
+
|
|
926
|
+
Returns:
|
|
927
|
+
Number of bit errors (Hamming distance).
|
|
928
|
+
"""
|
|
929
|
+
# Pad shorter sequence
|
|
930
|
+
max_len = max(len(received), len(expected))
|
|
931
|
+
received_padded = received + b"\x00" * (max_len - len(received))
|
|
932
|
+
expected_padded = expected + b"\x00" * (max_len - len(expected))
|
|
933
|
+
|
|
934
|
+
bit_errors = 0
|
|
935
|
+
for r, e in zip(received_padded, expected_padded, strict=True):
|
|
936
|
+
# Count differing bits
|
|
937
|
+
xor = r ^ e
|
|
938
|
+
while xor:
|
|
939
|
+
bit_errors += xor & 1
|
|
940
|
+
xor >>= 1
|
|
941
|
+
|
|
942
|
+
return bit_errors
|
|
943
|
+
|
|
944
|
+
def _export_pcap(self) -> None:
|
|
945
|
+
"""Export captured traffic to PCAP file.
|
|
946
|
+
|
|
947
|
+
Requires scapy to be installed.
|
|
948
|
+
"""
|
|
949
|
+
if wrpcap is None or IP is None or UDP is None or Packet is None:
|
|
950
|
+
# Silently skip if scapy not available
|
|
951
|
+
return
|
|
952
|
+
|
|
953
|
+
packets: list[Any] = []
|
|
954
|
+
for timestamp, sent_data, recv_data in self._pcap_packets:
|
|
955
|
+
# Create UDP packet for sent data
|
|
956
|
+
pkt = IP(dst="192.168.1.1") / UDP(dport=12345) / bytes(sent_data)
|
|
957
|
+
pkt.time = timestamp
|
|
958
|
+
packets.append(pkt)
|
|
959
|
+
|
|
960
|
+
# Create UDP packet for received data if present
|
|
961
|
+
if recv_data:
|
|
962
|
+
pkt = IP(src="192.168.1.1") / UDP(sport=12345) / bytes(recv_data)
|
|
963
|
+
pkt.time = timestamp + 0.001 # Slight offset
|
|
964
|
+
packets.append(pkt)
|
|
965
|
+
|
|
966
|
+
if packets:
|
|
967
|
+
wrpcap(self.config.pcap_file, packets)
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
__all__ = [
|
|
971
|
+
"HILConfig",
|
|
972
|
+
"HILTestReport",
|
|
973
|
+
"HILTestResult",
|
|
974
|
+
"HILTester",
|
|
975
|
+
"InterfaceType",
|
|
976
|
+
"TestStatus",
|
|
977
|
+
]
|