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,446 @@
|
|
|
1
|
+
"""OPC UA data type parsers.
|
|
2
|
+
|
|
3
|
+
This module implements parsing for OPC UA built-in data types including
|
|
4
|
+
NodeId, Variant, String, DateTime, and other primitives.
|
|
5
|
+
|
|
6
|
+
References:
|
|
7
|
+
OPC UA Part 6: Mappings - Section 5.1 Built-in Types
|
|
8
|
+
https://reference.opcfoundation.org/Core/Part6/v105/docs/5.1
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_string(data: bytes, offset: int) -> tuple[str | None, int]:
|
|
17
|
+
"""Parse OPC UA String (length-prefixed UTF-8).
|
|
18
|
+
|
|
19
|
+
String Format:
|
|
20
|
+
- Length (4 bytes, little-endian, -1 for null)
|
|
21
|
+
- Data (Length bytes, UTF-8 encoded)
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
data: Binary data containing the string.
|
|
25
|
+
offset: Starting offset in data.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Tuple of (parsed_string, bytes_consumed).
|
|
29
|
+
Returns (None, 4) for null string.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> data = b'\\x05\\x00\\x00\\x00Hello'
|
|
33
|
+
>>> s, consumed = parse_string(data, 0)
|
|
34
|
+
>>> assert s == "Hello"
|
|
35
|
+
>>> assert consumed == 9
|
|
36
|
+
"""
|
|
37
|
+
if offset + 4 > len(data):
|
|
38
|
+
return None, 0
|
|
39
|
+
|
|
40
|
+
length = int.from_bytes(data[offset : offset + 4], "little", signed=True)
|
|
41
|
+
consumed = 4
|
|
42
|
+
|
|
43
|
+
if length == -1:
|
|
44
|
+
# Null string
|
|
45
|
+
return None, consumed
|
|
46
|
+
|
|
47
|
+
if length < 0:
|
|
48
|
+
# Invalid length
|
|
49
|
+
return None, consumed
|
|
50
|
+
|
|
51
|
+
if offset + 4 + length > len(data):
|
|
52
|
+
# Not enough data
|
|
53
|
+
return None, consumed
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
string_value = data[offset + 4 : offset + 4 + length].decode("utf-8")
|
|
57
|
+
except UnicodeDecodeError:
|
|
58
|
+
# Invalid UTF-8
|
|
59
|
+
string_value = data[offset + 4 : offset + 4 + length].decode("utf-8", errors="replace")
|
|
60
|
+
|
|
61
|
+
consumed += length
|
|
62
|
+
return string_value, consumed
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_node_id(data: bytes, offset: int) -> tuple[str, int]:
|
|
66
|
+
"""Parse OPC UA NodeId.
|
|
67
|
+
|
|
68
|
+
NodeId Encoding:
|
|
69
|
+
- EncodingByte (1 byte):
|
|
70
|
+
* Bits 0-5: Encoding type
|
|
71
|
+
- 0x00: TwoByte (ns=0, numeric id < 256)
|
|
72
|
+
- 0x01: FourByte (ns < 256, numeric id < 65536)
|
|
73
|
+
- 0x02: Numeric (full 32-bit namespace and identifier)
|
|
74
|
+
- 0x03: String (namespace + UTF-8 string)
|
|
75
|
+
- 0x04: Guid (namespace + 16-byte GUID)
|
|
76
|
+
- 0x05: ByteString (namespace + byte string)
|
|
77
|
+
* Bits 6-7: Namespace URI and Server Index flags
|
|
78
|
+
- Namespace (varies by encoding)
|
|
79
|
+
- Identifier (varies by encoding)
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
data: Binary data containing the NodeId.
|
|
83
|
+
offset: Starting offset in data.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Tuple of (node_id_string, bytes_consumed).
|
|
87
|
+
NodeId string formats:
|
|
88
|
+
- Numeric: "ns=X;i=Y" or "i=Y" (if ns=0)
|
|
89
|
+
- String: "ns=X;s=string"
|
|
90
|
+
- Guid: "ns=X;g=guid"
|
|
91
|
+
- ByteString: "ns=X;b=base64"
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
>>> # FourByte numeric NodeId: ns=2, id=1001
|
|
95
|
+
>>> data = bytes([0x01, 0x02, 0xE9, 0x03])
|
|
96
|
+
>>> node_id, consumed = parse_node_id(data, 0)
|
|
97
|
+
>>> assert node_id == "ns=2;i=1001"
|
|
98
|
+
>>> assert consumed == 4
|
|
99
|
+
"""
|
|
100
|
+
if offset >= len(data):
|
|
101
|
+
return "i=0", 0
|
|
102
|
+
|
|
103
|
+
encoding_byte = data[offset]
|
|
104
|
+
encoding_type = encoding_byte & 0x3F
|
|
105
|
+
consumed = 1
|
|
106
|
+
|
|
107
|
+
if encoding_type == 0x00:
|
|
108
|
+
return _parse_twobyte_nodeid(data, offset, consumed)
|
|
109
|
+
elif encoding_type == 0x01:
|
|
110
|
+
return _parse_fourbyte_nodeid(data, offset, consumed)
|
|
111
|
+
elif encoding_type == 0x02:
|
|
112
|
+
return _parse_numeric_nodeid(data, offset, consumed)
|
|
113
|
+
elif encoding_type == 0x03:
|
|
114
|
+
return _parse_string_nodeid(data, offset, consumed)
|
|
115
|
+
elif encoding_type == 0x04:
|
|
116
|
+
return _parse_guid_nodeid(data, offset, consumed)
|
|
117
|
+
elif encoding_type == 0x05:
|
|
118
|
+
return _parse_bytestring_nodeid(data, offset, consumed)
|
|
119
|
+
else:
|
|
120
|
+
return "i=0", consumed
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _parse_twobyte_nodeid(data: bytes, offset: int, consumed: int) -> tuple[str, int]:
|
|
124
|
+
"""Parse TwoByte NodeId (ns=0, identifier < 256).
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
data: Binary data.
|
|
128
|
+
offset: Starting offset.
|
|
129
|
+
consumed: Bytes already consumed.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Tuple of (node_id_string, total_bytes_consumed).
|
|
133
|
+
"""
|
|
134
|
+
if offset + 1 >= len(data):
|
|
135
|
+
return "i=0", consumed
|
|
136
|
+
identifier = data[offset + 1]
|
|
137
|
+
return f"i={identifier}", consumed + 1
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _parse_fourbyte_nodeid(data: bytes, offset: int, consumed: int) -> tuple[str, int]:
|
|
141
|
+
"""Parse FourByte NodeId (ns < 256, identifier < 65536).
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
data: Binary data.
|
|
145
|
+
offset: Starting offset.
|
|
146
|
+
consumed: Bytes already consumed.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Tuple of (node_id_string, total_bytes_consumed).
|
|
150
|
+
"""
|
|
151
|
+
if offset + 3 >= len(data):
|
|
152
|
+
return "i=0", consumed
|
|
153
|
+
|
|
154
|
+
namespace = data[offset + 1]
|
|
155
|
+
identifier = int.from_bytes(data[offset + 2 : offset + 4], "little")
|
|
156
|
+
consumed += 3
|
|
157
|
+
|
|
158
|
+
return _format_numeric_nodeid(namespace, identifier), consumed
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _parse_numeric_nodeid(data: bytes, offset: int, consumed: int) -> tuple[str, int]:
|
|
162
|
+
"""Parse Numeric NodeId (full 32-bit namespace and identifier).
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
data: Binary data.
|
|
166
|
+
offset: Starting offset.
|
|
167
|
+
consumed: Bytes already consumed.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Tuple of (node_id_string, total_bytes_consumed).
|
|
171
|
+
"""
|
|
172
|
+
if offset + 6 >= len(data):
|
|
173
|
+
return "i=0", consumed
|
|
174
|
+
|
|
175
|
+
namespace = int.from_bytes(data[offset + 1 : offset + 3], "little")
|
|
176
|
+
identifier = int.from_bytes(data[offset + 3 : offset + 7], "little")
|
|
177
|
+
consumed += 6
|
|
178
|
+
|
|
179
|
+
return _format_numeric_nodeid(namespace, identifier), consumed
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _parse_string_nodeid(data: bytes, offset: int, consumed: int) -> tuple[str, int]:
|
|
183
|
+
"""Parse String NodeId (namespace + UTF-8 string).
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
data: Binary data.
|
|
187
|
+
offset: Starting offset.
|
|
188
|
+
consumed: Bytes already consumed.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Tuple of (node_id_string, total_bytes_consumed).
|
|
192
|
+
"""
|
|
193
|
+
if offset + 2 >= len(data):
|
|
194
|
+
return "i=0", consumed
|
|
195
|
+
|
|
196
|
+
namespace = int.from_bytes(data[offset + 1 : offset + 3], "little")
|
|
197
|
+
consumed += 2
|
|
198
|
+
|
|
199
|
+
string_value, string_consumed = parse_string(data, offset + consumed)
|
|
200
|
+
consumed += string_consumed
|
|
201
|
+
|
|
202
|
+
if string_value is None:
|
|
203
|
+
return "i=0", consumed
|
|
204
|
+
|
|
205
|
+
if namespace == 0:
|
|
206
|
+
return f"s={string_value}", consumed
|
|
207
|
+
return f"ns={namespace};s={string_value}", consumed
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _parse_guid_nodeid(data: bytes, offset: int, consumed: int) -> tuple[str, int]:
|
|
211
|
+
"""Parse Guid NodeId (namespace + 16-byte GUID).
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
data: Binary data.
|
|
215
|
+
offset: Starting offset.
|
|
216
|
+
consumed: Bytes already consumed.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Tuple of (node_id_string, total_bytes_consumed).
|
|
220
|
+
"""
|
|
221
|
+
if offset + 18 >= len(data):
|
|
222
|
+
return "i=0", consumed
|
|
223
|
+
|
|
224
|
+
namespace = int.from_bytes(data[offset + 1 : offset + 3], "little")
|
|
225
|
+
guid_bytes = data[offset + 3 : offset + 19]
|
|
226
|
+
consumed += 18
|
|
227
|
+
guid_str = guid_bytes.hex()
|
|
228
|
+
|
|
229
|
+
if namespace == 0:
|
|
230
|
+
return f"g={guid_str}", consumed
|
|
231
|
+
return f"ns={namespace};g={guid_str}", consumed
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _parse_bytestring_nodeid(data: bytes, offset: int, consumed: int) -> tuple[str, int]:
|
|
235
|
+
"""Parse ByteString NodeId (namespace + length-prefixed byte string).
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
data: Binary data.
|
|
239
|
+
offset: Starting offset.
|
|
240
|
+
consumed: Bytes already consumed.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Tuple of (node_id_string, total_bytes_consumed).
|
|
244
|
+
"""
|
|
245
|
+
if offset + 2 >= len(data):
|
|
246
|
+
return "i=0", consumed
|
|
247
|
+
|
|
248
|
+
namespace = int.from_bytes(data[offset + 1 : offset + 3], "little")
|
|
249
|
+
consumed += 2
|
|
250
|
+
|
|
251
|
+
bs_value, bs_consumed = parse_string(data, offset + consumed)
|
|
252
|
+
consumed += bs_consumed
|
|
253
|
+
|
|
254
|
+
if bs_value is None:
|
|
255
|
+
return "i=0", consumed
|
|
256
|
+
|
|
257
|
+
if namespace == 0:
|
|
258
|
+
return f"b={bs_value}", consumed
|
|
259
|
+
return f"ns={namespace};b={bs_value}", consumed
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _format_numeric_nodeid(namespace: int, identifier: int) -> str:
|
|
263
|
+
"""Format numeric NodeId as string.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
namespace: Namespace index.
|
|
267
|
+
identifier: Numeric identifier.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Formatted NodeId string.
|
|
271
|
+
"""
|
|
272
|
+
if namespace == 0:
|
|
273
|
+
return f"i={identifier}"
|
|
274
|
+
return f"ns={namespace};i={identifier}"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def parse_variant(data: bytes, offset: int) -> tuple[Any, int]:
|
|
278
|
+
"""Parse OPC UA Variant data type.
|
|
279
|
+
|
|
280
|
+
Variant Encoding:
|
|
281
|
+
- EncodingByte (1 byte):
|
|
282
|
+
* Bits 0-5: Data type
|
|
283
|
+
* Bit 6: Array flag
|
|
284
|
+
* Bit 7: Array dimensions flag
|
|
285
|
+
- Value (varies by type)
|
|
286
|
+
- Array length (if array flag set)
|
|
287
|
+
- Array dimensions (if dimensions flag set)
|
|
288
|
+
|
|
289
|
+
Built-in type IDs:
|
|
290
|
+
- 1: Boolean
|
|
291
|
+
- 2: SByte
|
|
292
|
+
- 3: Byte
|
|
293
|
+
- 4: Int16
|
|
294
|
+
- 5: UInt16
|
|
295
|
+
- 6: Int32
|
|
296
|
+
- 7: UInt32
|
|
297
|
+
- 8: Int64
|
|
298
|
+
- 9: UInt64
|
|
299
|
+
- 10: Float
|
|
300
|
+
- 11: Double
|
|
301
|
+
- 12: String
|
|
302
|
+
- 13: DateTime
|
|
303
|
+
- 15: Guid
|
|
304
|
+
- 17: NodeId
|
|
305
|
+
- 22: LocalizedText
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
data: Binary data containing the variant.
|
|
309
|
+
offset: Starting offset in data.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Tuple of (parsed_value, bytes_consumed).
|
|
313
|
+
|
|
314
|
+
Example:
|
|
315
|
+
>>> # UInt32 variant
|
|
316
|
+
>>> data = bytes([0x07, 0x2A, 0x00, 0x00, 0x00])
|
|
317
|
+
>>> value, consumed = parse_variant(data, 0)
|
|
318
|
+
>>> assert value == 42
|
|
319
|
+
>>> assert consumed == 5
|
|
320
|
+
"""
|
|
321
|
+
if offset >= len(data):
|
|
322
|
+
return None, 0
|
|
323
|
+
|
|
324
|
+
encoding_byte = data[offset]
|
|
325
|
+
type_id = encoding_byte & 0x3F
|
|
326
|
+
is_array = bool(encoding_byte & 0x40)
|
|
327
|
+
consumed = 1
|
|
328
|
+
|
|
329
|
+
# Handle arrays (simplified - just return indication)
|
|
330
|
+
if is_array:
|
|
331
|
+
return {"array": True, "type_id": type_id}, consumed
|
|
332
|
+
|
|
333
|
+
# Parse scalar value based on type
|
|
334
|
+
value, bytes_read = _parse_variant_scalar(data, offset + 1, type_id)
|
|
335
|
+
consumed += bytes_read
|
|
336
|
+
|
|
337
|
+
return value, consumed
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _parse_variant_scalar(data: bytes, offset: int, type_id: int) -> tuple[Any, int]:
|
|
341
|
+
"""Parse a scalar variant value.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
data: Binary data.
|
|
345
|
+
offset: Starting offset (after encoding byte).
|
|
346
|
+
type_id: Variant type ID.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Tuple of (parsed_value, bytes_consumed).
|
|
350
|
+
"""
|
|
351
|
+
# Boolean (1 byte)
|
|
352
|
+
if type_id == 1:
|
|
353
|
+
return _parse_boolean_variant(data, offset)
|
|
354
|
+
|
|
355
|
+
# Byte (1 byte)
|
|
356
|
+
elif type_id == 3:
|
|
357
|
+
return _parse_byte_variant(data, offset)
|
|
358
|
+
|
|
359
|
+
# Int16 (2 bytes)
|
|
360
|
+
elif type_id == 4:
|
|
361
|
+
return _parse_int16_variant(data, offset)
|
|
362
|
+
|
|
363
|
+
# UInt16 (2 bytes)
|
|
364
|
+
elif type_id == 5:
|
|
365
|
+
return _parse_uint16_variant(data, offset)
|
|
366
|
+
|
|
367
|
+
# Int32 (4 bytes)
|
|
368
|
+
elif type_id == 6:
|
|
369
|
+
return _parse_int32_variant(data, offset)
|
|
370
|
+
|
|
371
|
+
# UInt32 (4 bytes)
|
|
372
|
+
elif type_id == 7:
|
|
373
|
+
return _parse_uint32_variant(data, offset)
|
|
374
|
+
|
|
375
|
+
# String (length-prefixed)
|
|
376
|
+
elif type_id == 12:
|
|
377
|
+
return _parse_string_variant(data, offset)
|
|
378
|
+
|
|
379
|
+
# NodeId
|
|
380
|
+
elif type_id == 17:
|
|
381
|
+
return _parse_nodeid_variant(data, offset)
|
|
382
|
+
|
|
383
|
+
# Unsupported types - return type indicator
|
|
384
|
+
else:
|
|
385
|
+
return {"type_id": type_id, "unsupported": True}, 0
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _parse_boolean_variant(data: bytes, offset: int) -> tuple[bool | None, int]:
|
|
389
|
+
"""Parse Boolean variant (1 byte)."""
|
|
390
|
+
if offset < len(data):
|
|
391
|
+
return bool(data[offset]), 1
|
|
392
|
+
return None, 0
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _parse_byte_variant(data: bytes, offset: int) -> tuple[int | None, int]:
|
|
396
|
+
"""Parse Byte variant (1 byte)."""
|
|
397
|
+
if offset < len(data):
|
|
398
|
+
return data[offset], 1
|
|
399
|
+
return None, 0
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _parse_int16_variant(data: bytes, offset: int) -> tuple[int | None, int]:
|
|
403
|
+
"""Parse Int16 variant (2 bytes)."""
|
|
404
|
+
if offset + 1 < len(data):
|
|
405
|
+
value = int.from_bytes(data[offset : offset + 2], "little", signed=True)
|
|
406
|
+
return value, 2
|
|
407
|
+
return None, 0
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _parse_uint16_variant(data: bytes, offset: int) -> tuple[int | None, int]:
|
|
411
|
+
"""Parse UInt16 variant (2 bytes)."""
|
|
412
|
+
if offset + 1 < len(data):
|
|
413
|
+
value = int.from_bytes(data[offset : offset + 2], "little")
|
|
414
|
+
return value, 2
|
|
415
|
+
return None, 0
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _parse_int32_variant(data: bytes, offset: int) -> tuple[int | None, int]:
|
|
419
|
+
"""Parse Int32 variant (4 bytes)."""
|
|
420
|
+
if offset + 3 < len(data):
|
|
421
|
+
value = int.from_bytes(data[offset : offset + 4], "little", signed=True)
|
|
422
|
+
return value, 4
|
|
423
|
+
return None, 0
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _parse_uint32_variant(data: bytes, offset: int) -> tuple[int | None, int]:
|
|
427
|
+
"""Parse UInt32 variant (4 bytes)."""
|
|
428
|
+
if offset + 3 < len(data):
|
|
429
|
+
value = int.from_bytes(data[offset : offset + 4], "little")
|
|
430
|
+
return value, 4
|
|
431
|
+
return None, 0
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _parse_string_variant(data: bytes, offset: int) -> tuple[str | None, int]:
|
|
435
|
+
"""Parse String variant (length-prefixed)."""
|
|
436
|
+
str_value, str_consumed = parse_string(data, offset)
|
|
437
|
+
return str_value, str_consumed
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _parse_nodeid_variant(data: bytes, offset: int) -> tuple[str, int]:
|
|
441
|
+
"""Parse NodeId variant."""
|
|
442
|
+
node_id, node_consumed = parse_node_id(data, offset)
|
|
443
|
+
return node_id, node_consumed
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
__all__ = ["parse_node_id", "parse_string", "parse_variant"]
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""OPC UA service request/response decoders.
|
|
2
|
+
|
|
3
|
+
This module implements parsing for OPC UA service-specific payloads including
|
|
4
|
+
Read, Write, Browse, Subscribe, and Publish operations.
|
|
5
|
+
|
|
6
|
+
References:
|
|
7
|
+
OPC UA Part 4: Services
|
|
8
|
+
https://reference.opcfoundation.org/Core/Part4/v105/docs/
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_read_request(data: bytes) -> dict[str, Any]:
|
|
18
|
+
"""Parse ReadRequest service payload.
|
|
19
|
+
|
|
20
|
+
ReadRequest Format:
|
|
21
|
+
- RequestHeader (complex structure)
|
|
22
|
+
- MaxAge (8 bytes, double)
|
|
23
|
+
- TimestampsToReturn (4 bytes, enum)
|
|
24
|
+
- NodesToRead (array of ReadValueId)
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
data: Service payload bytes.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Parsed request data.
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
>>> # Simplified ReadRequest
|
|
34
|
+
>>> request = parse_read_request(b'...')
|
|
35
|
+
>>> assert 'nodes_to_read' in request
|
|
36
|
+
"""
|
|
37
|
+
# Simplified implementation - full parsing would be very complex
|
|
38
|
+
result: dict[str, Any] = {
|
|
39
|
+
"service": "ReadRequest",
|
|
40
|
+
"payload_size": len(data),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Try to extract basic information
|
|
44
|
+
if len(data) >= 12:
|
|
45
|
+
# Skip request header (would need complex parsing)
|
|
46
|
+
# MaxAge at some offset
|
|
47
|
+
# TimestampsToReturn enum
|
|
48
|
+
result["partial_parse"] = True
|
|
49
|
+
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse_read_response(data: bytes) -> dict[str, Any]:
|
|
54
|
+
"""Parse ReadResponse service payload.
|
|
55
|
+
|
|
56
|
+
ReadResponse Format:
|
|
57
|
+
- ResponseHeader (complex structure)
|
|
58
|
+
- Results (array of DataValue)
|
|
59
|
+
- DiagnosticInfos (array, optional)
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
data: Service payload bytes.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Parsed response data.
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
>>> response = parse_read_response(b'...')
|
|
69
|
+
>>> assert 'service' in response
|
|
70
|
+
"""
|
|
71
|
+
result: dict[str, Any] = {
|
|
72
|
+
"service": "ReadResponse",
|
|
73
|
+
"payload_size": len(data),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def parse_write_request(data: bytes) -> dict[str, Any]:
|
|
80
|
+
"""Parse WriteRequest service payload.
|
|
81
|
+
|
|
82
|
+
WriteRequest Format:
|
|
83
|
+
- RequestHeader
|
|
84
|
+
- NodesToWrite (array of WriteValue)
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
data: Service payload bytes.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Parsed request data.
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
>>> request = parse_write_request(b'...')
|
|
94
|
+
>>> assert request['service'] == 'WriteRequest'
|
|
95
|
+
"""
|
|
96
|
+
result: dict[str, Any] = {
|
|
97
|
+
"service": "WriteRequest",
|
|
98
|
+
"payload_size": len(data),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse_browse_request(data: bytes) -> dict[str, Any]:
|
|
105
|
+
"""Parse BrowseRequest service payload.
|
|
106
|
+
|
|
107
|
+
BrowseRequest Format:
|
|
108
|
+
- RequestHeader
|
|
109
|
+
- View (ViewDescription)
|
|
110
|
+
- RequestedMaxReferencesPerNode (4 bytes)
|
|
111
|
+
- NodesToBrowse (array of BrowseDescription)
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
data: Service payload bytes.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Parsed request data.
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
>>> request = parse_browse_request(b'...')
|
|
121
|
+
>>> assert 'service' in request
|
|
122
|
+
"""
|
|
123
|
+
result: dict[str, Any] = {
|
|
124
|
+
"service": "BrowseRequest",
|
|
125
|
+
"payload_size": len(data),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def parse_browse_response(data: bytes) -> dict[str, Any]:
|
|
132
|
+
"""Parse BrowseResponse service payload.
|
|
133
|
+
|
|
134
|
+
BrowseResponse Format:
|
|
135
|
+
- ResponseHeader
|
|
136
|
+
- Results (array of BrowseResult)
|
|
137
|
+
- DiagnosticInfos (array, optional)
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
data: Service payload bytes.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Parsed response data.
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
>>> response = parse_browse_response(b'...')
|
|
147
|
+
>>> assert response['service'] == 'BrowseResponse'
|
|
148
|
+
"""
|
|
149
|
+
result: dict[str, Any] = {
|
|
150
|
+
"service": "BrowseResponse",
|
|
151
|
+
"payload_size": len(data),
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def parse_create_subscription_request(data: bytes) -> dict[str, Any]:
|
|
158
|
+
"""Parse CreateSubscriptionRequest service payload.
|
|
159
|
+
|
|
160
|
+
CreateSubscriptionRequest Format:
|
|
161
|
+
- RequestHeader
|
|
162
|
+
- RequestedPublishingInterval (8 bytes, double)
|
|
163
|
+
- RequestedLifetimeCount (4 bytes)
|
|
164
|
+
- RequestedMaxKeepAliveCount (4 bytes)
|
|
165
|
+
- MaxNotificationsPerPublish (4 bytes)
|
|
166
|
+
- PublishingEnabled (1 byte, boolean)
|
|
167
|
+
- Priority (1 byte)
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
data: Service payload bytes.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Parsed request data with subscription parameters.
|
|
174
|
+
|
|
175
|
+
Example:
|
|
176
|
+
>>> request = parse_create_subscription_request(b'...')
|
|
177
|
+
>>> assert 'service' in request
|
|
178
|
+
"""
|
|
179
|
+
result: dict[str, Any] = {
|
|
180
|
+
"service": "CreateSubscriptionRequest",
|
|
181
|
+
"payload_size": len(data),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return result
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def parse_publish_request(data: bytes) -> dict[str, Any]:
|
|
188
|
+
"""Parse PublishRequest service payload.
|
|
189
|
+
|
|
190
|
+
PublishRequest Format:
|
|
191
|
+
- RequestHeader
|
|
192
|
+
- SubscriptionAcknowledgements (array)
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
data: Service payload bytes.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Parsed request data.
|
|
199
|
+
|
|
200
|
+
Example:
|
|
201
|
+
>>> request = parse_publish_request(b'...')
|
|
202
|
+
>>> assert request['service'] == 'PublishRequest'
|
|
203
|
+
"""
|
|
204
|
+
result: dict[str, Any] = {
|
|
205
|
+
"service": "PublishRequest",
|
|
206
|
+
"payload_size": len(data),
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return result
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def parse_publish_response(data: bytes) -> dict[str, Any]:
|
|
213
|
+
"""Parse PublishResponse service payload.
|
|
214
|
+
|
|
215
|
+
PublishResponse Format:
|
|
216
|
+
- ResponseHeader
|
|
217
|
+
- SubscriptionId (4 bytes)
|
|
218
|
+
- AvailableSequenceNumbers (array)
|
|
219
|
+
- MoreNotifications (1 byte, boolean)
|
|
220
|
+
- NotificationMessage (complex)
|
|
221
|
+
- Results (array of StatusCode)
|
|
222
|
+
- DiagnosticInfos (array, optional)
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
data: Service payload bytes.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Parsed response data with notification data.
|
|
229
|
+
|
|
230
|
+
Example:
|
|
231
|
+
>>> response = parse_publish_response(b'...')
|
|
232
|
+
>>> assert 'service' in response
|
|
233
|
+
"""
|
|
234
|
+
result: dict[str, Any] = {
|
|
235
|
+
"service": "PublishResponse",
|
|
236
|
+
"payload_size": len(data),
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return result
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# Service ID to parser mapping
|
|
243
|
+
SERVICE_PARSERS: dict[
|
|
244
|
+
int, tuple[Callable[[bytes], dict[str, Any]] | None, Callable[[bytes], dict[str, Any]] | None]
|
|
245
|
+
] = {
|
|
246
|
+
421: (parse_read_request, parse_read_response), # Read
|
|
247
|
+
673: (parse_write_request, None), # Write (response is simple StatusCode array)
|
|
248
|
+
527: (parse_browse_request, parse_browse_response), # Browse
|
|
249
|
+
631: (parse_create_subscription_request, None), # CreateSubscription
|
|
250
|
+
826: (parse_publish_request, parse_publish_response), # Publish
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
__all__ = [
|
|
255
|
+
"SERVICE_PARSERS",
|
|
256
|
+
"parse_browse_request",
|
|
257
|
+
"parse_browse_response",
|
|
258
|
+
"parse_create_subscription_request",
|
|
259
|
+
"parse_publish_request",
|
|
260
|
+
"parse_publish_response",
|
|
261
|
+
"parse_read_request",
|
|
262
|
+
"parse_read_response",
|
|
263
|
+
"parse_write_request",
|
|
264
|
+
]
|