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,23 @@
|
|
|
1
|
+
"""PROFINET IO protocol analyzer package.
|
|
2
|
+
|
|
3
|
+
This package provides comprehensive PROFINET protocol analysis for real-time
|
|
4
|
+
industrial Ethernet communication, including RT, IRT, DCP, and PTCP protocols.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from oscura.analyzers.protocols.industrial.profinet import ProfinetAnalyzer
|
|
8
|
+
>>> analyzer = ProfinetAnalyzer()
|
|
9
|
+
>>> frame = analyzer.parse_frame(ethernet_data, timestamp=0.0)
|
|
10
|
+
>>> devices = analyzer.discover_devices()
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
PROFINET Specification V2.4 (IEC 61158 / IEC 61784):
|
|
14
|
+
https://www.profibus.com/download/profinet-specification/
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from oscura.analyzers.protocols.industrial.profinet.analyzer import (
|
|
18
|
+
ProfinetAnalyzer,
|
|
19
|
+
ProfinetDevice,
|
|
20
|
+
ProfinetFrame,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = ["ProfinetAnalyzer", "ProfinetDevice", "ProfinetFrame"]
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"""PROFINET IO protocol analyzer.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive PROFINET protocol analysis for real-time
|
|
4
|
+
industrial Ethernet communication. Supports RT (Real-Time), IRT (Isochronous
|
|
5
|
+
Real-Time), DCP (Discovery and Configuration), and PTCP (Precision Time) protocols.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.analyzers.protocols.industrial.profinet import ProfinetAnalyzer
|
|
9
|
+
>>> analyzer = ProfinetAnalyzer()
|
|
10
|
+
>>> # Parse Ethernet frame containing PROFINET data
|
|
11
|
+
>>> frame = analyzer.parse_frame(ethernet_frame, timestamp=0.0)
|
|
12
|
+
>>> print(f"Frame Type: {frame.frame_type}, Frame ID: 0x{frame.frame_id:04X}")
|
|
13
|
+
>>> # Discover devices from DCP Identify responses
|
|
14
|
+
>>> devices = analyzer.discover_devices()
|
|
15
|
+
>>> for device in devices:
|
|
16
|
+
... print(f"Device: {device.device_name} at {device.mac_address}")
|
|
17
|
+
>>> # Export topology
|
|
18
|
+
>>> analyzer.export_topology(Path("profinet_topology.json"))
|
|
19
|
+
|
|
20
|
+
References:
|
|
21
|
+
PROFINET Specification V2.4:
|
|
22
|
+
https://www.profibus.com/download/profinet-specification/
|
|
23
|
+
|
|
24
|
+
IEC 61158 / IEC 61784 (Industrial communication networks)
|
|
25
|
+
Wireshark PROFINET dissector
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any, ClassVar
|
|
34
|
+
|
|
35
|
+
from oscura.analyzers.protocols.industrial.profinet.dcp import DCPParser
|
|
36
|
+
from oscura.analyzers.protocols.industrial.profinet.ptcp import PTCPParser
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ProfinetFrame:
|
|
41
|
+
"""PROFINET frame representation.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
timestamp: Frame timestamp in seconds.
|
|
45
|
+
frame_type: Frame type name (e.g., "RT_CLASS_1", "DCP", "PTCP").
|
|
46
|
+
frame_id: PROFINET frame ID (0x8000-0xFFFF).
|
|
47
|
+
source_mac: Source MAC address (XX:XX:XX:XX:XX:XX format).
|
|
48
|
+
dest_mac: Destination MAC address.
|
|
49
|
+
cycle_counter: Cycle counter for RT frames.
|
|
50
|
+
data_status: Data status byte for RT frames.
|
|
51
|
+
payload: Raw payload bytes.
|
|
52
|
+
decoded: Decoded frame data (varies by frame type).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
timestamp: float
|
|
56
|
+
frame_type: str
|
|
57
|
+
frame_id: int
|
|
58
|
+
source_mac: str
|
|
59
|
+
dest_mac: str
|
|
60
|
+
cycle_counter: int | None = None
|
|
61
|
+
data_status: int | None = None
|
|
62
|
+
payload: bytes = b""
|
|
63
|
+
decoded: dict[str, Any] = field(default_factory=dict)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class ProfinetDevice:
|
|
68
|
+
"""PROFINET device information.
|
|
69
|
+
|
|
70
|
+
Attributes:
|
|
71
|
+
mac_address: Device MAC address.
|
|
72
|
+
device_name: Device name from DCP (Name of Station).
|
|
73
|
+
device_type: Device type description.
|
|
74
|
+
vendor_id: Vendor ID from DCP Device ID block.
|
|
75
|
+
device_id: Device ID from DCP Device ID block.
|
|
76
|
+
station_type: Station type (IO-Controller, IO-Device, etc.).
|
|
77
|
+
modules: List of device modules/submodules.
|
|
78
|
+
ip_address: Device IP address (if configured).
|
|
79
|
+
subnet_mask: Subnet mask.
|
|
80
|
+
gateway: Gateway address.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
mac_address: str
|
|
84
|
+
device_name: str | None = None
|
|
85
|
+
device_type: str | None = None
|
|
86
|
+
vendor_id: int | None = None
|
|
87
|
+
device_id: int | None = None
|
|
88
|
+
station_type: str = "DEVICE"
|
|
89
|
+
modules: list[dict[str, Any]] = field(default_factory=list)
|
|
90
|
+
ip_address: str | None = None
|
|
91
|
+
subnet_mask: str | None = None
|
|
92
|
+
gateway: str | None = None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ProfinetAnalyzer:
|
|
96
|
+
"""PROFINET IO protocol analyzer.
|
|
97
|
+
|
|
98
|
+
Provides comprehensive analysis of PROFINET frames including RT (Real-Time),
|
|
99
|
+
IRT (Isochronous Real-Time), DCP (Discovery and Configuration Protocol),
|
|
100
|
+
and PTCP (Precision Transparent Clock Protocol).
|
|
101
|
+
|
|
102
|
+
Attributes:
|
|
103
|
+
frames: List of parsed PROFINET frames.
|
|
104
|
+
devices: Dictionary of discovered devices by MAC address.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
>>> analyzer = ProfinetAnalyzer()
|
|
108
|
+
>>> # Parse frame from raw Ethernet data
|
|
109
|
+
>>> frame = analyzer.parse_frame(eth_frame, timestamp=1.234)
|
|
110
|
+
>>> # Discover devices
|
|
111
|
+
>>> devices = analyzer.discover_devices()
|
|
112
|
+
>>> print(f"Found {len(devices)} PROFINET devices")
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
# PROFINET Frame ID ranges (0x0000-0xFFFF)
|
|
116
|
+
FRAME_ID_RANGES: ClassVar[list[tuple[tuple[int, int], str]]] = [
|
|
117
|
+
((0x0000, 0x7FFF), "Reserved"),
|
|
118
|
+
((0x8000, 0xBFFF), "RT_CLASS_1"), # Cyclic Real-Time data
|
|
119
|
+
((0xC000, 0xFBFF), "RT_CLASS_UDP"), # RT over UDP
|
|
120
|
+
((0xFC00, 0xFCFF), "RT_CLASS_2"), # IRT
|
|
121
|
+
((0xFD00, 0xFDFF), "RT_CLASS_3"), # IRT with fragmentation
|
|
122
|
+
((0xFE00, 0xFEFF), "Reserved for profiles"),
|
|
123
|
+
((0xFF00, 0xFF1F), "Multicast MAC range"),
|
|
124
|
+
((0xFF20, 0xFF3F), "PTCP"), # Precision Time Protocol - Delay
|
|
125
|
+
((0xFF40, 0xFF4F), "PTCP"), # Precision Time Protocol - Sync
|
|
126
|
+
((0xFF50, 0xFF8F), "PTCP"), # Reserved for PTCP
|
|
127
|
+
((0xFF90, 0xFFFF), "Reserved"),
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
# PROFINET EtherType
|
|
131
|
+
ETHERTYPE_PROFINET: ClassVar[int] = 0x8892
|
|
132
|
+
|
|
133
|
+
def __init__(self) -> None:
|
|
134
|
+
"""Initialize PROFINET analyzer."""
|
|
135
|
+
self.frames: list[ProfinetFrame] = []
|
|
136
|
+
self.devices: dict[str, ProfinetDevice] = {}
|
|
137
|
+
self._dcp_parser = DCPParser()
|
|
138
|
+
self._ptcp_parser = PTCPParser()
|
|
139
|
+
|
|
140
|
+
def parse_frame(self, ethernet_frame: bytes, timestamp: float = 0.0) -> ProfinetFrame:
|
|
141
|
+
"""Parse PROFINET frame from Ethernet payload.
|
|
142
|
+
|
|
143
|
+
Extracts PROFINET-specific data from raw Ethernet frame and decodes
|
|
144
|
+
based on frame type (RT, DCP, PTCP, etc.).
|
|
145
|
+
|
|
146
|
+
Ethernet Frame Format:
|
|
147
|
+
- Destination MAC (6 bytes)
|
|
148
|
+
- Source MAC (6 bytes)
|
|
149
|
+
- EtherType/Length (2 bytes) - should be 0x8892 for PROFINET
|
|
150
|
+
- PROFINET Data (variable)
|
|
151
|
+
- FCS (4 bytes, typically stripped by capture)
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
ethernet_frame: Complete Ethernet frame including headers.
|
|
155
|
+
timestamp: Frame timestamp in seconds.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Parsed PROFINET frame.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
ValueError: If frame is too short or not a valid PROFINET frame.
|
|
162
|
+
|
|
163
|
+
Example:
|
|
164
|
+
>>> analyzer = ProfinetAnalyzer()
|
|
165
|
+
>>> eth_frame = bytes([...]) # Raw Ethernet frame
|
|
166
|
+
>>> frame = analyzer.parse_frame(eth_frame, timestamp=1.5)
|
|
167
|
+
>>> print(f"Frame type: {frame.frame_type}")
|
|
168
|
+
"""
|
|
169
|
+
if len(ethernet_frame) < 14:
|
|
170
|
+
raise ValueError(f"Ethernet frame too short: {len(ethernet_frame)} bytes (minimum 14)")
|
|
171
|
+
|
|
172
|
+
# Parse Ethernet header and extract PROFINET payload
|
|
173
|
+
dest_mac, source_mac, profinet_data = self._parse_ethernet_header(ethernet_frame)
|
|
174
|
+
|
|
175
|
+
# Extract Frame ID and classify
|
|
176
|
+
frame_id = int.from_bytes(profinet_data[0:2], "big")
|
|
177
|
+
frame_type = self._classify_frame_id(frame_id)
|
|
178
|
+
frame_payload = profinet_data[2:]
|
|
179
|
+
|
|
180
|
+
# Decode frame based on type (may update frame_type for DCP)
|
|
181
|
+
frame_type, decoded, cycle_counter, data_status = self._decode_frame_payload(
|
|
182
|
+
frame_id, frame_type, frame_payload
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
frame = ProfinetFrame(
|
|
186
|
+
timestamp=timestamp,
|
|
187
|
+
frame_type=frame_type,
|
|
188
|
+
frame_id=frame_id,
|
|
189
|
+
source_mac=source_mac,
|
|
190
|
+
dest_mac=dest_mac,
|
|
191
|
+
cycle_counter=cycle_counter,
|
|
192
|
+
data_status=data_status,
|
|
193
|
+
payload=frame_payload,
|
|
194
|
+
decoded=decoded,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
self.frames.append(frame)
|
|
198
|
+
self._update_device_info(frame)
|
|
199
|
+
return frame
|
|
200
|
+
|
|
201
|
+
def _parse_ethernet_header(self, ethernet_frame: bytes) -> tuple[str, str, bytes]:
|
|
202
|
+
"""Parse Ethernet header and extract PROFINET payload."""
|
|
203
|
+
dest_mac = ":".join(f"{b:02x}" for b in ethernet_frame[0:6])
|
|
204
|
+
source_mac = ":".join(f"{b:02x}" for b in ethernet_frame[6:12])
|
|
205
|
+
ethertype = int.from_bytes(ethernet_frame[12:14], "big")
|
|
206
|
+
|
|
207
|
+
# Handle VLAN-tagged frames
|
|
208
|
+
payload_offset = 14
|
|
209
|
+
if ethertype == 0x8100:
|
|
210
|
+
if len(ethernet_frame) < 18:
|
|
211
|
+
raise ValueError("VLAN-tagged frame too short")
|
|
212
|
+
ethertype = int.from_bytes(ethernet_frame[16:18], "big")
|
|
213
|
+
payload_offset = 18
|
|
214
|
+
|
|
215
|
+
if ethertype != self.ETHERTYPE_PROFINET:
|
|
216
|
+
raise ValueError(
|
|
217
|
+
f"Not a PROFINET frame: EtherType 0x{ethertype:04X} "
|
|
218
|
+
f"(expected 0x{self.ETHERTYPE_PROFINET:04X})"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
profinet_data = ethernet_frame[payload_offset:]
|
|
222
|
+
if len(profinet_data) < 2:
|
|
223
|
+
raise ValueError(f"PROFINET data too short: {len(profinet_data)} bytes")
|
|
224
|
+
|
|
225
|
+
return dest_mac, source_mac, profinet_data
|
|
226
|
+
|
|
227
|
+
def _decode_frame_payload(
|
|
228
|
+
self, frame_id: int, frame_type: str, frame_payload: bytes
|
|
229
|
+
) -> tuple[str, dict[str, Any], int | None, int | None]:
|
|
230
|
+
"""Decode frame payload based on frame type.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Tuple of (frame_type, decoded, cycle_counter, data_status).
|
|
234
|
+
frame_type may be updated for DCP frames.
|
|
235
|
+
"""
|
|
236
|
+
decoded: dict[str, Any] = {}
|
|
237
|
+
cycle_counter, data_status = None, None
|
|
238
|
+
|
|
239
|
+
if frame_type.startswith("RT_CLASS"):
|
|
240
|
+
decoded = self._parse_rt_frame(frame_id, frame_payload)
|
|
241
|
+
cycle_counter = decoded.get("cycle_counter")
|
|
242
|
+
data_status = decoded.get("data_status")
|
|
243
|
+
|
|
244
|
+
elif frame_type == "PTCP":
|
|
245
|
+
try:
|
|
246
|
+
decoded = self._ptcp_parser.parse_frame(frame_id, frame_payload)
|
|
247
|
+
except ValueError as e:
|
|
248
|
+
decoded = {"parse_error": str(e)}
|
|
249
|
+
|
|
250
|
+
elif frame_id in (0xFEFC, 0xFEFD):
|
|
251
|
+
frame_type = "DCP"
|
|
252
|
+
try:
|
|
253
|
+
decoded = self._dcp_parser.parse_frame(frame_payload)
|
|
254
|
+
except ValueError as e:
|
|
255
|
+
decoded = {"parse_error": str(e)}
|
|
256
|
+
|
|
257
|
+
return frame_type, decoded, cycle_counter, data_status
|
|
258
|
+
|
|
259
|
+
def _classify_frame_id(self, frame_id: int) -> str:
|
|
260
|
+
"""Classify frame based on Frame ID range.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
frame_id: PROFINET frame ID (0x0000-0xFFFF).
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Frame type classification string.
|
|
267
|
+
"""
|
|
268
|
+
for (start, end), frame_type in self.FRAME_ID_RANGES:
|
|
269
|
+
if start <= frame_id <= end:
|
|
270
|
+
return frame_type
|
|
271
|
+
return f"Unknown (0x{frame_id:04X})"
|
|
272
|
+
|
|
273
|
+
def _parse_rt_frame(self, frame_id: int, data: bytes) -> dict[str, Any]:
|
|
274
|
+
"""Parse PROFINET Real-Time frame.
|
|
275
|
+
|
|
276
|
+
RT Frame Format:
|
|
277
|
+
- Cycle Counter (2 bytes) - for RT_CLASS_1 and higher
|
|
278
|
+
- Data Status (1 byte)
|
|
279
|
+
- Transfer Status (1 byte) - optional for some classes
|
|
280
|
+
- I/O Data (variable)
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
frame_id: PROFINET frame ID.
|
|
284
|
+
data: RT frame payload.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Parsed RT frame data.
|
|
288
|
+
"""
|
|
289
|
+
if len(data) < 4:
|
|
290
|
+
return {"error": "RT frame too short", "raw_data": data.hex()}
|
|
291
|
+
|
|
292
|
+
result: dict[str, Any] = {"frame_id": frame_id}
|
|
293
|
+
|
|
294
|
+
# Check if this is cyclic data (has cycle counter)
|
|
295
|
+
if 0x8000 <= frame_id <= 0xFDFF:
|
|
296
|
+
cycle_counter = int.from_bytes(data[0:2], "big")
|
|
297
|
+
data_status = data[2]
|
|
298
|
+
io_data = data[3:]
|
|
299
|
+
|
|
300
|
+
# Parse Data Status byte (IEC 61158-6-10)
|
|
301
|
+
# Bit 7: State (0=BACKUP, 1=PRIMARY)
|
|
302
|
+
# Bit 6: Redundancy (0=No redundancy, 1=Redundancy enabled)
|
|
303
|
+
# Bit 5: DataValid (0=Invalid, 1=Valid)
|
|
304
|
+
# Bit 4: Reserved
|
|
305
|
+
# Bit 3: Provider State (0=STOP, 1=RUN)
|
|
306
|
+
# Bit 2: Station Problem Indicator (0=Normal, 1=Problem)
|
|
307
|
+
# Bit 1: Reserved
|
|
308
|
+
# Bit 0: Reserved
|
|
309
|
+
|
|
310
|
+
result.update(
|
|
311
|
+
{
|
|
312
|
+
"cycle_counter": cycle_counter,
|
|
313
|
+
"data_status": data_status,
|
|
314
|
+
"data_status_flags": {
|
|
315
|
+
"primary": bool(data_status & 0x80),
|
|
316
|
+
"redundancy": bool(data_status & 0x40),
|
|
317
|
+
"data_valid": bool(data_status & 0x20),
|
|
318
|
+
"provider_state": "RUN" if (data_status & 0x08) else "STOP",
|
|
319
|
+
"station_problem": bool(data_status & 0x04),
|
|
320
|
+
},
|
|
321
|
+
"io_data": io_data.hex(),
|
|
322
|
+
"io_data_length": len(io_data),
|
|
323
|
+
}
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
return result
|
|
327
|
+
|
|
328
|
+
def _update_device_info(self, frame: ProfinetFrame) -> None:
|
|
329
|
+
"""Update device information from parsed frame.
|
|
330
|
+
|
|
331
|
+
Extracts and updates device information from DCP frames.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
frame: Parsed PROFINET frame.
|
|
335
|
+
"""
|
|
336
|
+
if frame.frame_type != "DCP" or "blocks" not in frame.decoded:
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
# Extract device information from DCP blocks
|
|
340
|
+
mac = frame.source_mac
|
|
341
|
+
if mac not in self.devices:
|
|
342
|
+
self.devices[mac] = ProfinetDevice(mac_address=mac)
|
|
343
|
+
|
|
344
|
+
device = self.devices[mac]
|
|
345
|
+
|
|
346
|
+
for block in frame.decoded.get("blocks", []):
|
|
347
|
+
# Device name
|
|
348
|
+
if "device_name" in block:
|
|
349
|
+
device.device_name = block["device_name"]
|
|
350
|
+
|
|
351
|
+
# Device ID (Vendor ID + Device ID)
|
|
352
|
+
if "vendor_id" in block and "device_id" in block:
|
|
353
|
+
device.vendor_id = block["vendor_id"]
|
|
354
|
+
device.device_id = block["device_id"]
|
|
355
|
+
|
|
356
|
+
# Device role
|
|
357
|
+
if "role_names" in block:
|
|
358
|
+
roles = block["role_names"]
|
|
359
|
+
if "IO-Controller" in roles:
|
|
360
|
+
device.station_type = "IO-Controller"
|
|
361
|
+
elif "IO-Device" in roles:
|
|
362
|
+
device.station_type = "IO-Device"
|
|
363
|
+
elif "IO-Supervisor" in roles:
|
|
364
|
+
device.station_type = "IO-Supervisor"
|
|
365
|
+
|
|
366
|
+
# IP configuration
|
|
367
|
+
if "ip_address" in block:
|
|
368
|
+
device.ip_address = block["ip_address"]
|
|
369
|
+
if "subnet_mask" in block:
|
|
370
|
+
device.subnet_mask = block["subnet_mask"]
|
|
371
|
+
if "gateway" in block:
|
|
372
|
+
device.gateway = block["gateway"]
|
|
373
|
+
|
|
374
|
+
def discover_devices(self) -> list[ProfinetDevice]:
|
|
375
|
+
"""Discover PROFINET devices from DCP Identify responses.
|
|
376
|
+
|
|
377
|
+
Analyzes all parsed frames to extract device information from
|
|
378
|
+
DCP protocol messages.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
List of discovered PROFINET devices.
|
|
382
|
+
|
|
383
|
+
Example:
|
|
384
|
+
>>> analyzer = ProfinetAnalyzer()
|
|
385
|
+
>>> # ... parse frames ...
|
|
386
|
+
>>> devices = analyzer.discover_devices()
|
|
387
|
+
>>> for device in devices:
|
|
388
|
+
... print(f"{device.mac_address}: {device.device_name}")
|
|
389
|
+
"""
|
|
390
|
+
return list(self.devices.values())
|
|
391
|
+
|
|
392
|
+
def export_topology(self, output_path: Path) -> None:
|
|
393
|
+
"""Export device topology as JSON.
|
|
394
|
+
|
|
395
|
+
Exports all discovered devices and their configuration to a JSON file.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
output_path: Path to output JSON file.
|
|
399
|
+
|
|
400
|
+
Example:
|
|
401
|
+
>>> analyzer = ProfinetAnalyzer()
|
|
402
|
+
>>> # ... parse frames and discover devices ...
|
|
403
|
+
>>> analyzer.export_topology(Path("profinet_network.json"))
|
|
404
|
+
"""
|
|
405
|
+
topology = {
|
|
406
|
+
"network_type": "PROFINET IO",
|
|
407
|
+
"devices": [
|
|
408
|
+
{
|
|
409
|
+
"mac_address": device.mac_address,
|
|
410
|
+
"device_name": device.device_name,
|
|
411
|
+
"device_type": device.device_type,
|
|
412
|
+
"vendor_id": device.vendor_id,
|
|
413
|
+
"device_id": device.device_id,
|
|
414
|
+
"station_type": device.station_type,
|
|
415
|
+
"ip_address": device.ip_address,
|
|
416
|
+
"subnet_mask": device.subnet_mask,
|
|
417
|
+
"gateway": device.gateway,
|
|
418
|
+
"modules": device.modules,
|
|
419
|
+
}
|
|
420
|
+
for device in self.devices.values()
|
|
421
|
+
],
|
|
422
|
+
"frame_count": len(self.frames),
|
|
423
|
+
"frame_types": self._get_frame_type_statistics(),
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
with output_path.open("w") as f:
|
|
427
|
+
json.dump(topology, f, indent=2)
|
|
428
|
+
|
|
429
|
+
def _get_frame_type_statistics(self) -> dict[str, int]:
|
|
430
|
+
"""Get statistics of frame types seen.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
Dictionary mapping frame type to count.
|
|
434
|
+
"""
|
|
435
|
+
stats: dict[str, int] = {}
|
|
436
|
+
for frame in self.frames:
|
|
437
|
+
stats[frame.frame_type] = stats.get(frame.frame_type, 0) + 1
|
|
438
|
+
return stats
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
__all__ = ["ProfinetAnalyzer", "ProfinetDevice", "ProfinetFrame"]
|