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,622 @@
|
|
|
1
|
+
"""BACnet service decoders.
|
|
2
|
+
|
|
3
|
+
This module provides service-specific decoders for BACnet confirmed and unconfirmed
|
|
4
|
+
services according to ASHRAE 135-2020.
|
|
5
|
+
|
|
6
|
+
References:
|
|
7
|
+
ANSI/ASHRAE Standard 135-2020, Clause 15-19: Application Layer Services
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from oscura.analyzers.protocols.industrial.bacnet.encoding import (
|
|
15
|
+
parse_application_tag,
|
|
16
|
+
parse_object_identifier,
|
|
17
|
+
parse_tag,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def decode_who_is(data: bytes) -> dict[str, Any]:
|
|
22
|
+
"""Decode Who-Is service (unconfirmed service for device discovery).
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
data: Service payload bytes.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Dictionary with optional device_instance_range_low_limit and
|
|
29
|
+
device_instance_range_high_limit if specified.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> data = bytes([0x09, 0x00, 0x19, 0xFF]) # Range 0-255
|
|
33
|
+
>>> result = decode_who_is(data)
|
|
34
|
+
"""
|
|
35
|
+
result: dict[str, Any] = {}
|
|
36
|
+
offset = 0
|
|
37
|
+
|
|
38
|
+
# Optional device instance range (context tags 0 and 1)
|
|
39
|
+
if offset < len(data):
|
|
40
|
+
try:
|
|
41
|
+
tag, tag_size = parse_tag(data, offset)
|
|
42
|
+
if tag["context_specific"] and tag["tag_number"] == 0:
|
|
43
|
+
# Low limit
|
|
44
|
+
value_offset = offset + tag_size
|
|
45
|
+
low_limit = int.from_bytes(data[value_offset : value_offset + tag["length"]], "big")
|
|
46
|
+
result["device_instance_range_low_limit"] = low_limit
|
|
47
|
+
offset = value_offset + tag["length"]
|
|
48
|
+
|
|
49
|
+
# High limit (context tag 1)
|
|
50
|
+
if offset < len(data):
|
|
51
|
+
tag, tag_size = parse_tag(data, offset)
|
|
52
|
+
if tag["context_specific"] and tag["tag_number"] == 1:
|
|
53
|
+
value_offset = offset + tag_size
|
|
54
|
+
high_limit = int.from_bytes(
|
|
55
|
+
data[value_offset : value_offset + tag["length"]], "big"
|
|
56
|
+
)
|
|
57
|
+
result["device_instance_range_high_limit"] = high_limit
|
|
58
|
+
except (ValueError, IndexError):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def decode_i_am(data: bytes) -> dict[str, Any]:
|
|
65
|
+
"""Decode I-Am service (unconfirmed device announcement).
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
data: Service payload bytes.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Dictionary with device_instance, max_apdu_length, segmentation, and vendor_id.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
>>> data = bytes([0xC4, 0x02, 0x00, 0x00, 0x08, 0x22, 0x05, 0x00, ...])
|
|
75
|
+
>>> result = decode_i_am(data)
|
|
76
|
+
>>> print(f"Device {result['device_instance']}")
|
|
77
|
+
"""
|
|
78
|
+
result: dict[str, Any] = {}
|
|
79
|
+
offset = 0
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
# Device object identifier (application tag 12, object identifier)
|
|
83
|
+
value, consumed = parse_application_tag(data, offset)
|
|
84
|
+
if isinstance(value, dict) and "instance" in value:
|
|
85
|
+
result["device_instance"] = value["instance"]
|
|
86
|
+
result["device_object_type"] = value.get("object_type_name", "unknown")
|
|
87
|
+
offset += consumed
|
|
88
|
+
|
|
89
|
+
# Max APDU length accepted (unsigned)
|
|
90
|
+
if offset < len(data):
|
|
91
|
+
value, consumed = parse_application_tag(data, offset)
|
|
92
|
+
result["max_apdu_length"] = value
|
|
93
|
+
offset += consumed
|
|
94
|
+
|
|
95
|
+
# Segmentation supported (enumerated)
|
|
96
|
+
if offset < len(data):
|
|
97
|
+
value, consumed = parse_application_tag(data, offset)
|
|
98
|
+
segmentation_names = {
|
|
99
|
+
0: "both",
|
|
100
|
+
1: "transmit",
|
|
101
|
+
2: "receive",
|
|
102
|
+
3: "no-segmentation",
|
|
103
|
+
}
|
|
104
|
+
result["segmentation"] = segmentation_names.get(value, value)
|
|
105
|
+
offset += consumed
|
|
106
|
+
|
|
107
|
+
# Vendor ID (unsigned)
|
|
108
|
+
if offset < len(data):
|
|
109
|
+
value, consumed = parse_application_tag(data, offset)
|
|
110
|
+
result["vendor_id"] = value
|
|
111
|
+
offset += consumed
|
|
112
|
+
|
|
113
|
+
except (ValueError, IndexError):
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def decode_who_has(data: bytes) -> dict[str, Any]:
|
|
120
|
+
"""Decode Who-Has service (unconfirmed service to find objects).
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
data: Service payload bytes.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Dictionary with optional device range and object identifier or name.
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
>>> data = bytes([0x3C, 0x02, 0x00, 0x00, 0x01, 0x3E, ...])
|
|
130
|
+
>>> result = decode_who_has(data)
|
|
131
|
+
"""
|
|
132
|
+
result: dict[str, Any] = {}
|
|
133
|
+
offset = 0
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
# Optional device instance range (context tags 0 and 1)
|
|
137
|
+
tag, tag_size = parse_tag(data, offset)
|
|
138
|
+
if tag["context_specific"] and tag["tag_number"] == 0:
|
|
139
|
+
value_offset = offset + tag_size
|
|
140
|
+
low_limit = int.from_bytes(data[value_offset : value_offset + tag["length"]], "big")
|
|
141
|
+
result["device_instance_range_low_limit"] = low_limit
|
|
142
|
+
offset = value_offset + tag["length"]
|
|
143
|
+
|
|
144
|
+
tag, tag_size = parse_tag(data, offset)
|
|
145
|
+
if tag["context_specific"] and tag["tag_number"] == 1:
|
|
146
|
+
value_offset = offset + tag_size
|
|
147
|
+
high_limit = int.from_bytes(
|
|
148
|
+
data[value_offset : value_offset + tag["length"]], "big"
|
|
149
|
+
)
|
|
150
|
+
result["device_instance_range_high_limit"] = high_limit
|
|
151
|
+
offset = value_offset + tag["length"]
|
|
152
|
+
|
|
153
|
+
# Object identifier or object name (choice: context 2 or 3)
|
|
154
|
+
if offset < len(data):
|
|
155
|
+
tag, tag_size = parse_tag(data, offset)
|
|
156
|
+
if tag["context_specific"] and tag["tag_number"] == 2:
|
|
157
|
+
# Object identifier
|
|
158
|
+
obj_id, _ = parse_object_identifier(data, offset + tag_size)
|
|
159
|
+
result["object_identifier"] = obj_id
|
|
160
|
+
elif tag["context_specific"] and tag["tag_number"] == 3:
|
|
161
|
+
# Object name
|
|
162
|
+
value_offset = offset + tag_size
|
|
163
|
+
name = data[value_offset : value_offset + tag["length"]].decode(
|
|
164
|
+
"utf-8", errors="replace"
|
|
165
|
+
)
|
|
166
|
+
result["object_name"] = name
|
|
167
|
+
|
|
168
|
+
except (ValueError, IndexError):
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def decode_i_have(data: bytes) -> dict[str, Any]:
|
|
175
|
+
"""Decode I-Have service (unconfirmed response to Who-Has).
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
data: Service payload bytes.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Dictionary with device_identifier, object_identifier, and object_name.
|
|
182
|
+
|
|
183
|
+
Example:
|
|
184
|
+
>>> result = decode_i_have(data)
|
|
185
|
+
>>> print(f"Device has {result['object_name']}")
|
|
186
|
+
"""
|
|
187
|
+
result: dict[str, Any] = {}
|
|
188
|
+
offset = 0
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
# Device identifier
|
|
192
|
+
value, consumed = parse_application_tag(data, offset)
|
|
193
|
+
result["device_identifier"] = value
|
|
194
|
+
offset += consumed
|
|
195
|
+
|
|
196
|
+
# Object identifier
|
|
197
|
+
value, consumed = parse_application_tag(data, offset)
|
|
198
|
+
result["object_identifier"] = value
|
|
199
|
+
offset += consumed
|
|
200
|
+
|
|
201
|
+
# Object name
|
|
202
|
+
value, consumed = parse_application_tag(data, offset)
|
|
203
|
+
result["object_name"] = value
|
|
204
|
+
offset += consumed
|
|
205
|
+
|
|
206
|
+
except (ValueError, IndexError):
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
return result
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def decode_read_property_request(data: bytes) -> dict[str, Any]:
|
|
213
|
+
"""Decode ReadProperty service request.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
data: Service payload bytes.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Dictionary with object_identifier, property_identifier, and optional
|
|
220
|
+
property_array_index.
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
>>> result = decode_read_property_request(data)
|
|
224
|
+
>>> print(f"Read {result['property_identifier']} from {result['object_identifier']}")
|
|
225
|
+
"""
|
|
226
|
+
result: dict[str, Any] = {}
|
|
227
|
+
offset = 0
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
# Object identifier (context tag 0)
|
|
231
|
+
tag, tag_size = parse_tag(data, offset)
|
|
232
|
+
if tag["context_specific"] and tag["tag_number"] == 0:
|
|
233
|
+
obj_id, _ = parse_object_identifier(data, offset + tag_size)
|
|
234
|
+
result["object_identifier"] = obj_id
|
|
235
|
+
offset += tag_size + 4
|
|
236
|
+
|
|
237
|
+
# Property identifier (context tag 1)
|
|
238
|
+
if offset < len(data):
|
|
239
|
+
tag, tag_size = parse_tag(data, offset)
|
|
240
|
+
if tag["context_specific"] and tag["tag_number"] == 1:
|
|
241
|
+
value_offset = offset + tag_size
|
|
242
|
+
prop_id = int.from_bytes(data[value_offset : value_offset + tag["length"]], "big")
|
|
243
|
+
result["property_identifier"] = prop_id
|
|
244
|
+
result["property_name"] = get_property_name(prop_id)
|
|
245
|
+
offset = value_offset + tag["length"]
|
|
246
|
+
|
|
247
|
+
# Optional property array index (context tag 2)
|
|
248
|
+
if offset < len(data):
|
|
249
|
+
tag, tag_size = parse_tag(data, offset)
|
|
250
|
+
if tag["context_specific"] and tag["tag_number"] == 2:
|
|
251
|
+
value_offset = offset + tag_size
|
|
252
|
+
array_index = int.from_bytes(
|
|
253
|
+
data[value_offset : value_offset + tag["length"]], "big"
|
|
254
|
+
)
|
|
255
|
+
result["property_array_index"] = array_index
|
|
256
|
+
|
|
257
|
+
except (ValueError, IndexError):
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
return result
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def decode_read_property_ack(data: bytes) -> dict[str, Any]:
|
|
264
|
+
"""Decode ReadProperty-ACK service response.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
data: Service payload bytes.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Dictionary with object_identifier, property_identifier, and property_value.
|
|
271
|
+
|
|
272
|
+
Example:
|
|
273
|
+
>>> result = decode_read_property_ack(data)
|
|
274
|
+
>>> print(f"Value: {result['property_value']}")
|
|
275
|
+
"""
|
|
276
|
+
result: dict[str, Any] = {}
|
|
277
|
+
offset = 0
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
# Parse object identifier (context tag 0)
|
|
281
|
+
obj_id, offset = _parse_bacnet_object_id(data, offset)
|
|
282
|
+
if obj_id is not None:
|
|
283
|
+
result["object_identifier"] = obj_id
|
|
284
|
+
|
|
285
|
+
# Parse property identifier (context tag 1)
|
|
286
|
+
prop_id, prop_name, offset = _parse_bacnet_property_id(data, offset)
|
|
287
|
+
if prop_id is not None:
|
|
288
|
+
result["property_identifier"] = prop_id
|
|
289
|
+
result["property_name"] = prop_name
|
|
290
|
+
|
|
291
|
+
# Parse optional property array index (context tag 2)
|
|
292
|
+
array_index, offset = _parse_bacnet_array_index(data, offset)
|
|
293
|
+
if array_index is not None:
|
|
294
|
+
result["property_array_index"] = array_index
|
|
295
|
+
|
|
296
|
+
# Parse property value (context tag 3)
|
|
297
|
+
prop_value = _parse_bacnet_property_value(data, offset)
|
|
298
|
+
if prop_value is not None:
|
|
299
|
+
result["property_value"] = prop_value
|
|
300
|
+
|
|
301
|
+
except (ValueError, IndexError):
|
|
302
|
+
pass
|
|
303
|
+
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _parse_bacnet_object_id(data: bytes, offset: int) -> tuple[dict[str, Any] | None, int]:
|
|
308
|
+
"""Parse BACnet object identifier from context tag 0.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
data: Payload bytes.
|
|
312
|
+
offset: Current parsing offset.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Tuple of (object_id_dict, new_offset). None if tag not found.
|
|
316
|
+
"""
|
|
317
|
+
if offset >= len(data):
|
|
318
|
+
return (None, offset)
|
|
319
|
+
|
|
320
|
+
tag, tag_size = parse_tag(data, offset)
|
|
321
|
+
if tag["context_specific"] and tag["tag_number"] == 0:
|
|
322
|
+
obj_id, _ = parse_object_identifier(data, offset + tag_size)
|
|
323
|
+
return (obj_id, offset + tag_size + 4)
|
|
324
|
+
|
|
325
|
+
return (None, offset)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _parse_bacnet_property_id(data: bytes, offset: int) -> tuple[int | None, str | None, int]:
|
|
329
|
+
"""Parse BACnet property identifier from context tag 1.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
data: Payload bytes.
|
|
333
|
+
offset: Current parsing offset.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Tuple of (property_id, property_name, new_offset). None if tag not found.
|
|
337
|
+
"""
|
|
338
|
+
if offset >= len(data):
|
|
339
|
+
return (None, None, offset)
|
|
340
|
+
|
|
341
|
+
tag, tag_size = parse_tag(data, offset)
|
|
342
|
+
if tag["context_specific"] and tag["tag_number"] == 1:
|
|
343
|
+
value_offset = offset + tag_size
|
|
344
|
+
prop_id = int.from_bytes(data[value_offset : value_offset + tag["length"]], "big")
|
|
345
|
+
prop_name = get_property_name(prop_id)
|
|
346
|
+
return (prop_id, prop_name, value_offset + tag["length"])
|
|
347
|
+
|
|
348
|
+
return (None, None, offset)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _parse_bacnet_array_index(data: bytes, offset: int) -> tuple[int | None, int]:
|
|
352
|
+
"""Parse optional BACnet array index from context tag 2.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
data: Payload bytes.
|
|
356
|
+
offset: Current parsing offset.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Tuple of (array_index, new_offset). None if tag not found.
|
|
360
|
+
"""
|
|
361
|
+
if offset >= len(data):
|
|
362
|
+
return (None, offset)
|
|
363
|
+
|
|
364
|
+
tag, tag_size = parse_tag(data, offset)
|
|
365
|
+
if tag["context_specific"] and tag["tag_number"] == 2:
|
|
366
|
+
value_offset = offset + tag_size
|
|
367
|
+
array_index = int.from_bytes(data[value_offset : value_offset + tag["length"]], "big")
|
|
368
|
+
return (array_index, value_offset + tag["length"])
|
|
369
|
+
|
|
370
|
+
return (None, offset)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _parse_bacnet_property_value(data: bytes, offset: int) -> Any:
|
|
374
|
+
"""Parse BACnet property value from context tag 3.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
data: Payload bytes.
|
|
378
|
+
offset: Current parsing offset.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Property value (single value or list). None if tag not found.
|
|
382
|
+
"""
|
|
383
|
+
if offset >= len(data):
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
tag, tag_size = parse_tag(data, offset)
|
|
387
|
+
if not (tag["context_specific"] and tag["tag_number"] == 3 and tag["is_opening"]):
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
offset += tag_size
|
|
391
|
+
|
|
392
|
+
# Parse values until closing tag
|
|
393
|
+
values = []
|
|
394
|
+
while offset < len(data):
|
|
395
|
+
tag, tag_size = parse_tag(data, offset)
|
|
396
|
+
if tag["is_closing"] and tag["tag_number"] == 3:
|
|
397
|
+
break
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
value, consumed = parse_application_tag(data, offset)
|
|
401
|
+
values.append(value)
|
|
402
|
+
offset += consumed
|
|
403
|
+
except ValueError:
|
|
404
|
+
# Skip unparseable data
|
|
405
|
+
offset += tag_size + tag.get("length", 0)
|
|
406
|
+
break
|
|
407
|
+
|
|
408
|
+
return values[0] if len(values) == 1 else (values if values else None)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def decode_write_property_request(data: bytes) -> dict[str, Any]:
|
|
412
|
+
"""Decode WriteProperty service request.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
data: Service payload bytes.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Dictionary with object_identifier, property_identifier, property_value,
|
|
419
|
+
and optional priority.
|
|
420
|
+
|
|
421
|
+
Example:
|
|
422
|
+
>>> result = decode_write_property_request(data)
|
|
423
|
+
>>> print(f"Write {result['property_value']} to {result['property_name']}")
|
|
424
|
+
"""
|
|
425
|
+
result: dict[str, Any] = {}
|
|
426
|
+
offset = 0
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
# Parse object identifier (context tag 0)
|
|
430
|
+
obj_id, offset = _parse_write_property_object_id(data, offset, result)
|
|
431
|
+
|
|
432
|
+
# Parse property identifier (context tag 1)
|
|
433
|
+
offset = _parse_write_property_id(data, offset, result)
|
|
434
|
+
|
|
435
|
+
# Parse optional property array index (context tag 2)
|
|
436
|
+
offset = _parse_write_property_array_index(data, offset, result)
|
|
437
|
+
|
|
438
|
+
# Parse property value (context tag 3)
|
|
439
|
+
offset = _parse_write_property_value(data, offset, result)
|
|
440
|
+
|
|
441
|
+
# Parse optional priority (context tag 4)
|
|
442
|
+
_parse_write_property_priority(data, offset, result)
|
|
443
|
+
|
|
444
|
+
except (ValueError, IndexError):
|
|
445
|
+
pass
|
|
446
|
+
|
|
447
|
+
return result
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _parse_write_property_object_id(
|
|
451
|
+
data: bytes, offset: int, result: dict[str, Any]
|
|
452
|
+
) -> tuple[dict[str, Any] | None, int]:
|
|
453
|
+
"""Parse object identifier from WriteProperty request.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
data: Payload bytes.
|
|
457
|
+
offset: Current offset.
|
|
458
|
+
result: Result dictionary to populate.
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
Tuple of (object_id, new_offset).
|
|
462
|
+
"""
|
|
463
|
+
tag, tag_size = parse_tag(data, offset)
|
|
464
|
+
if tag["context_specific"] and tag["tag_number"] == 0:
|
|
465
|
+
obj_id, _ = parse_object_identifier(data, offset + tag_size)
|
|
466
|
+
result["object_identifier"] = obj_id
|
|
467
|
+
return obj_id, offset + tag_size + 4
|
|
468
|
+
return None, offset
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _parse_write_property_id(data: bytes, offset: int, result: dict[str, Any]) -> int:
|
|
472
|
+
"""Parse property identifier from WriteProperty request.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
data: Payload bytes.
|
|
476
|
+
offset: Current offset.
|
|
477
|
+
result: Result dictionary to populate.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
New offset.
|
|
481
|
+
"""
|
|
482
|
+
if offset >= len(data):
|
|
483
|
+
return offset
|
|
484
|
+
|
|
485
|
+
tag, tag_size = parse_tag(data, offset)
|
|
486
|
+
if tag["context_specific"] and tag["tag_number"] == 1:
|
|
487
|
+
value_offset = offset + tag_size
|
|
488
|
+
length: int = int(tag["length"])
|
|
489
|
+
prop_id = int.from_bytes(data[value_offset : value_offset + length], "big")
|
|
490
|
+
result["property_identifier"] = prop_id
|
|
491
|
+
result["property_name"] = get_property_name(prop_id)
|
|
492
|
+
return value_offset + length
|
|
493
|
+
|
|
494
|
+
return offset
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _parse_write_property_array_index(data: bytes, offset: int, result: dict[str, Any]) -> int:
|
|
498
|
+
"""Parse optional array index from WriteProperty request.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
data: Payload bytes.
|
|
502
|
+
offset: Current offset.
|
|
503
|
+
result: Result dictionary to populate.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
New offset.
|
|
507
|
+
"""
|
|
508
|
+
if offset >= len(data):
|
|
509
|
+
return offset
|
|
510
|
+
|
|
511
|
+
tag, tag_size = parse_tag(data, offset)
|
|
512
|
+
if tag["context_specific"] and tag["tag_number"] == 2:
|
|
513
|
+
value_offset = offset + tag_size
|
|
514
|
+
length: int = int(tag["length"])
|
|
515
|
+
array_index = int.from_bytes(data[value_offset : value_offset + length], "big")
|
|
516
|
+
result["property_array_index"] = array_index
|
|
517
|
+
return value_offset + length
|
|
518
|
+
|
|
519
|
+
return offset
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _parse_write_property_value(data: bytes, offset: int, result: dict[str, Any]) -> int:
|
|
523
|
+
"""Parse property value from WriteProperty request.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
data: Payload bytes.
|
|
527
|
+
offset: Current offset.
|
|
528
|
+
result: Result dictionary to populate.
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
New offset.
|
|
532
|
+
"""
|
|
533
|
+
if offset >= len(data):
|
|
534
|
+
return offset
|
|
535
|
+
|
|
536
|
+
tag, tag_size = parse_tag(data, offset)
|
|
537
|
+
if tag["context_specific"] and tag["tag_number"] == 3 and tag["is_opening"]:
|
|
538
|
+
offset += tag_size
|
|
539
|
+
|
|
540
|
+
# Parse value(s) until closing tag
|
|
541
|
+
values = []
|
|
542
|
+
while offset < len(data):
|
|
543
|
+
tag, tag_size = parse_tag(data, offset)
|
|
544
|
+
if tag["is_closing"] and tag["tag_number"] == 3:
|
|
545
|
+
offset += tag_size
|
|
546
|
+
break
|
|
547
|
+
|
|
548
|
+
try:
|
|
549
|
+
value, consumed = parse_application_tag(data, offset)
|
|
550
|
+
values.append(value)
|
|
551
|
+
offset += consumed
|
|
552
|
+
except ValueError:
|
|
553
|
+
offset += tag_size + tag.get("length", 0)
|
|
554
|
+
break
|
|
555
|
+
|
|
556
|
+
result["property_value"] = values[0] if len(values) == 1 else values
|
|
557
|
+
|
|
558
|
+
return offset
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _parse_write_property_priority(data: bytes, offset: int, result: dict[str, Any]) -> None:
|
|
562
|
+
"""Parse optional priority from WriteProperty request.
|
|
563
|
+
|
|
564
|
+
Args:
|
|
565
|
+
data: Payload bytes.
|
|
566
|
+
offset: Current offset.
|
|
567
|
+
result: Result dictionary to populate.
|
|
568
|
+
"""
|
|
569
|
+
if offset >= len(data):
|
|
570
|
+
return
|
|
571
|
+
|
|
572
|
+
tag, tag_size = parse_tag(data, offset)
|
|
573
|
+
if tag["context_specific"] and tag["tag_number"] == 4:
|
|
574
|
+
value_offset = offset + tag_size
|
|
575
|
+
priority = int.from_bytes(data[value_offset : value_offset + tag["length"]], "big")
|
|
576
|
+
result["priority"] = priority
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def get_property_name(property_id: int) -> str:
|
|
580
|
+
"""Get human-readable property name from property identifier.
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
property_id: BACnet property identifier.
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
Property name string.
|
|
587
|
+
|
|
588
|
+
Example:
|
|
589
|
+
>>> name = get_property_name(85) # "present-value"
|
|
590
|
+
"""
|
|
591
|
+
# Common BACnet property identifiers (ASHRAE 135-2020, Clause 21)
|
|
592
|
+
property_names = {
|
|
593
|
+
0: "acked-transitions",
|
|
594
|
+
1: "ack-required",
|
|
595
|
+
4: "action",
|
|
596
|
+
8: "all",
|
|
597
|
+
28: "description",
|
|
598
|
+
36: "event-state",
|
|
599
|
+
41: "high-limit",
|
|
600
|
+
44: "limit-enable",
|
|
601
|
+
45: "local-date",
|
|
602
|
+
46: "local-time",
|
|
603
|
+
52: "low-limit",
|
|
604
|
+
56: "max-pres-value",
|
|
605
|
+
59: "min-pres-value",
|
|
606
|
+
62: "notify-type",
|
|
607
|
+
65: "object-identifier",
|
|
608
|
+
77: "object-name",
|
|
609
|
+
79: "object-type",
|
|
610
|
+
85: "present-value",
|
|
611
|
+
103: "reliability",
|
|
612
|
+
107: "segmentation-supported",
|
|
613
|
+
111: "status-flags",
|
|
614
|
+
112: "system-status",
|
|
615
|
+
117: "units",
|
|
616
|
+
120: "vendor-identifier",
|
|
617
|
+
121: "vendor-name",
|
|
618
|
+
122: "vt-classes-supported",
|
|
619
|
+
155: "event-enable",
|
|
620
|
+
158: "ack-mode",
|
|
621
|
+
}
|
|
622
|
+
return property_names.get(property_id, f"property-{property_id}")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""EtherCAT protocol analyzer package.
|
|
2
|
+
|
|
3
|
+
EtherCAT (Ethernet for Control Automation Technology) is a high-performance
|
|
4
|
+
industrial fieldbus system based on Ethernet.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from oscura.analyzers.protocols.industrial.ethercat import EtherCATAnalyzer
|
|
8
|
+
>>> analyzer = EtherCATAnalyzer()
|
|
9
|
+
>>> frame = analyzer.parse_frame(ethernet_payload, timestamp=0.0)
|
|
10
|
+
>>> print(f"Datagrams: {len(frame.datagrams)}")
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
IEC 61158 Type 12: https://www.iec.ch/
|
|
14
|
+
ETG.1000 EtherCAT Protocol Specification
|
|
15
|
+
ETG.2000 EtherCAT AL Protocol
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from oscura.analyzers.protocols.industrial.ethercat.analyzer import (
|
|
19
|
+
EtherCATAnalyzer,
|
|
20
|
+
EtherCATDatagram,
|
|
21
|
+
EtherCATFrame,
|
|
22
|
+
EtherCATSlave,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"EtherCATAnalyzer",
|
|
27
|
+
"EtherCATDatagram",
|
|
28
|
+
"EtherCATFrame",
|
|
29
|
+
"EtherCATSlave",
|
|
30
|
+
]
|