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,339 @@
|
|
|
1
|
+
"""EtherCAT mailbox protocol parsers.
|
|
2
|
+
|
|
3
|
+
This module provides parsers for EtherCAT mailbox protocols including
|
|
4
|
+
CoE (CAN over EtherCAT), FoE (File over EtherCAT), SoE (Servo over EtherCAT),
|
|
5
|
+
and EoE (Ethernet over EtherCAT).
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.analyzers.protocols.industrial.ethercat.mailbox import parse_mailbox
|
|
9
|
+
>>> mailbox_data = bytes([0x06, 0x00, 0x00, 0x00, 0x02, ...])
|
|
10
|
+
>>> result = parse_mailbox(mailbox_data)
|
|
11
|
+
>>> print(f"Protocol: {result['protocol']}")
|
|
12
|
+
|
|
13
|
+
References:
|
|
14
|
+
ETG.1000.6 CoE Protocol
|
|
15
|
+
ETG.8200 FoE Protocol
|
|
16
|
+
ETG.1000.4 SoE Protocol
|
|
17
|
+
ETG.8500 EoE Protocol
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def parse_mailbox(data: bytes) -> dict[str, Any]:
|
|
26
|
+
"""Parse EtherCAT mailbox protocol data.
|
|
27
|
+
|
|
28
|
+
Mailbox Header Format:
|
|
29
|
+
- Length (2 bytes, little-endian) - Data length
|
|
30
|
+
- Address (2 bytes, little-endian) - Station address
|
|
31
|
+
- Channel (6 bits) - Priority and channel info
|
|
32
|
+
- Priority (2 bits)
|
|
33
|
+
- Type (4 bits) - Protocol type
|
|
34
|
+
- Counter (3 bits)
|
|
35
|
+
- Reserved (1 bit)
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
data: Mailbox data bytes.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Parsed mailbox data dictionary.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError: If data is invalid.
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
>>> # CoE mailbox data
|
|
48
|
+
>>> data = bytes([0x0A, 0x00, 0x01, 0x00, 0x02, 0x00, ...])
|
|
49
|
+
>>> result = parse_mailbox(data)
|
|
50
|
+
>>> assert result['protocol'] == 'CoE'
|
|
51
|
+
"""
|
|
52
|
+
if len(data) < 6:
|
|
53
|
+
raise ValueError(f"Mailbox data too short: {len(data)} bytes (minimum 6)")
|
|
54
|
+
|
|
55
|
+
# Parse mailbox header
|
|
56
|
+
length = int.from_bytes(data[0:2], "little")
|
|
57
|
+
address = int.from_bytes(data[2:4], "little")
|
|
58
|
+
channel_priority = data[4]
|
|
59
|
+
type_counter = data[5]
|
|
60
|
+
|
|
61
|
+
# Extract fields
|
|
62
|
+
channel = channel_priority & 0x3F # Lower 6 bits
|
|
63
|
+
priority = (channel_priority >> 6) & 0x03 # Upper 2 bits
|
|
64
|
+
protocol_type = type_counter & 0x0F # Lower 4 bits
|
|
65
|
+
counter = (type_counter >> 4) & 0x07 # Bits 4-6
|
|
66
|
+
|
|
67
|
+
# Map protocol type
|
|
68
|
+
protocol_names = {
|
|
69
|
+
0x00: "ERR", # Error
|
|
70
|
+
0x02: "CoE", # CAN application protocol over EtherCAT
|
|
71
|
+
0x03: "FoE", # File access over EtherCAT
|
|
72
|
+
0x04: "SoE", # Servo drive profile over EtherCAT
|
|
73
|
+
0x05: "EoE", # Ethernet over EtherCAT
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
protocol = protocol_names.get(protocol_type, f"Unknown (0x{protocol_type:X})")
|
|
77
|
+
|
|
78
|
+
result: dict[str, Any] = {
|
|
79
|
+
"length": length,
|
|
80
|
+
"address": address,
|
|
81
|
+
"channel": channel,
|
|
82
|
+
"priority": priority,
|
|
83
|
+
"protocol": protocol,
|
|
84
|
+
"protocol_type": protocol_type,
|
|
85
|
+
"counter": counter,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Parse protocol-specific data
|
|
89
|
+
if len(data) > 6:
|
|
90
|
+
protocol_data = data[6:]
|
|
91
|
+
|
|
92
|
+
if protocol_type == 0x02: # CoE
|
|
93
|
+
result["coe"] = _parse_coe(protocol_data)
|
|
94
|
+
elif protocol_type == 0x03: # FoE
|
|
95
|
+
result["foe"] = _parse_foe(protocol_data)
|
|
96
|
+
elif protocol_type == 0x04: # SoE
|
|
97
|
+
result["soe"] = _parse_soe(protocol_data)
|
|
98
|
+
elif protocol_type == 0x05: # EoE
|
|
99
|
+
result["eoe"] = _parse_eoe(protocol_data)
|
|
100
|
+
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _parse_coe(data: bytes) -> dict[str, Any]:
|
|
105
|
+
"""Parse CoE (CAN over EtherCAT) protocol data.
|
|
106
|
+
|
|
107
|
+
CoE Header Format:
|
|
108
|
+
- Number (2 bytes, little-endian) - CAN ID or SDO index
|
|
109
|
+
- Service (4 bits) - CoE service type
|
|
110
|
+
- Reserved (4 bits)
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
data: CoE protocol data.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Parsed CoE data.
|
|
117
|
+
"""
|
|
118
|
+
if len(data) < 2:
|
|
119
|
+
return {"error": "CoE data too short"}
|
|
120
|
+
|
|
121
|
+
number = int.from_bytes(data[0:2], "little")
|
|
122
|
+
|
|
123
|
+
result: dict[str, Any] = {
|
|
124
|
+
"number": number,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if len(data) >= 3:
|
|
128
|
+
service = data[2] & 0x0F
|
|
129
|
+
|
|
130
|
+
# CoE service types
|
|
131
|
+
service_names = {
|
|
132
|
+
0x01: "SDO Request",
|
|
133
|
+
0x02: "SDO Response",
|
|
134
|
+
0x03: "TxPDO",
|
|
135
|
+
0x04: "RxPDO",
|
|
136
|
+
0x05: "TxPDO Remote Request",
|
|
137
|
+
0x06: "RxPDO Remote Request",
|
|
138
|
+
0x07: "SDO Information",
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
result["service"] = service_names.get(service, f"Unknown (0x{service:X})")
|
|
142
|
+
result["service_type"] = service
|
|
143
|
+
|
|
144
|
+
# Parse SDO data if present
|
|
145
|
+
if service in {0x01, 0x02} and len(data) >= 10: # SDO Request/Response
|
|
146
|
+
result["sdo"] = _parse_sdo(data[3:]) # Skip number (2 bytes) + service (1 byte)
|
|
147
|
+
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _parse_sdo(data: bytes) -> dict[str, Any]:
|
|
152
|
+
"""Parse SDO (Service Data Object) data.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
data: SDO data bytes.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Parsed SDO data.
|
|
159
|
+
"""
|
|
160
|
+
if len(data) < 8:
|
|
161
|
+
return {"error": "SDO data too short"}
|
|
162
|
+
|
|
163
|
+
command = data[0]
|
|
164
|
+
index = int.from_bytes(data[1:3], "little")
|
|
165
|
+
subindex = data[3]
|
|
166
|
+
data_bytes = data[4:8]
|
|
167
|
+
|
|
168
|
+
# SDO command specifiers
|
|
169
|
+
command_type = command >> 5 # Upper 3 bits
|
|
170
|
+
|
|
171
|
+
command_names = {
|
|
172
|
+
0x01: "Download Initiate",
|
|
173
|
+
0x02: "Download Segment",
|
|
174
|
+
0x03: "Upload Initiate",
|
|
175
|
+
0x04: "Upload Segment",
|
|
176
|
+
0x05: "Abort",
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
"command": command_names.get(command_type, f"Unknown (0x{command_type:X})"),
|
|
181
|
+
"command_code": command,
|
|
182
|
+
"index": index,
|
|
183
|
+
"subindex": subindex,
|
|
184
|
+
"data": data_bytes.hex(),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _parse_foe(data: bytes) -> dict[str, Any]:
|
|
189
|
+
"""Parse FoE (File over EtherCAT) protocol data.
|
|
190
|
+
|
|
191
|
+
FoE Header Format:
|
|
192
|
+
- OpCode (1 byte) - Operation code
|
|
193
|
+
- Reserved (1 byte)
|
|
194
|
+
- Packet Number (4 bytes, little-endian) - For data transfer
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
data: FoE protocol data.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Parsed FoE data.
|
|
201
|
+
"""
|
|
202
|
+
if len(data) < 1:
|
|
203
|
+
return {"error": "FoE data too short"}
|
|
204
|
+
|
|
205
|
+
opcode = data[0]
|
|
206
|
+
|
|
207
|
+
# FoE operation codes
|
|
208
|
+
opcode_names = {
|
|
209
|
+
0x01: "Read Request",
|
|
210
|
+
0x02: "Write Request",
|
|
211
|
+
0x03: "Data",
|
|
212
|
+
0x04: "Ack",
|
|
213
|
+
0x05: "Error",
|
|
214
|
+
0x06: "Busy",
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
result: dict[str, Any] = {
|
|
218
|
+
"opcode": opcode_names.get(opcode, f"Unknown (0x{opcode:02X})"),
|
|
219
|
+
"opcode_code": opcode,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if len(data) >= 6:
|
|
223
|
+
packet_number = int.from_bytes(data[2:6], "little")
|
|
224
|
+
result["packet_number"] = packet_number
|
|
225
|
+
|
|
226
|
+
return result
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _parse_soe(data: bytes) -> dict[str, Any]:
|
|
230
|
+
"""Parse SoE (Servo over EtherCAT) protocol data.
|
|
231
|
+
|
|
232
|
+
SoE Header Format:
|
|
233
|
+
- OpCode (3 bits) - Operation code
|
|
234
|
+
- InComplete (1 bit)
|
|
235
|
+
- Error (1 bit)
|
|
236
|
+
- DriveNo (3 bits)
|
|
237
|
+
- Elements (2 bytes, little-endian) - Number of elements
|
|
238
|
+
- IDN (2 bytes, little-endian) - Identification number
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
data: SoE protocol data.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Parsed SoE data.
|
|
245
|
+
"""
|
|
246
|
+
if len(data) < 4:
|
|
247
|
+
return {"error": "SoE data too short"}
|
|
248
|
+
|
|
249
|
+
header = data[0]
|
|
250
|
+
opcode = header & 0x07 # Lower 3 bits
|
|
251
|
+
incomplete = bool(header & 0x08) # Bit 3
|
|
252
|
+
error = bool(header & 0x10) # Bit 4
|
|
253
|
+
drive_no = (header >> 5) & 0x07 # Upper 3 bits
|
|
254
|
+
|
|
255
|
+
# SoE operation codes
|
|
256
|
+
opcode_names = {
|
|
257
|
+
0x00: "No Operation",
|
|
258
|
+
0x01: "Read Request",
|
|
259
|
+
0x02: "Read Response",
|
|
260
|
+
0x03: "Write Request",
|
|
261
|
+
0x04: "Write Response",
|
|
262
|
+
0x05: "Notification Request",
|
|
263
|
+
0x06: "Emergency",
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
result: dict[str, Any] = {
|
|
267
|
+
"opcode": opcode_names.get(opcode, f"Unknown (0x{opcode:X})"),
|
|
268
|
+
"opcode_code": opcode,
|
|
269
|
+
"incomplete": incomplete,
|
|
270
|
+
"error": error,
|
|
271
|
+
"drive_number": drive_no,
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if len(data) >= 4:
|
|
275
|
+
idn = int.from_bytes(data[2:4], "little")
|
|
276
|
+
result["idn"] = idn
|
|
277
|
+
|
|
278
|
+
return result
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _parse_eoe(data: bytes) -> dict[str, Any]:
|
|
282
|
+
"""Parse EoE (Ethernet over EtherCAT) protocol data.
|
|
283
|
+
|
|
284
|
+
EoE Header Format:
|
|
285
|
+
- Type (4 bits) - Fragment type
|
|
286
|
+
- Port (4 bits) - Port number
|
|
287
|
+
- Last Fragment (1 bit)
|
|
288
|
+
- Time Append (1 bit)
|
|
289
|
+
- Time Request (1 bit)
|
|
290
|
+
- Reserved (5 bits)
|
|
291
|
+
- Fragment Number (6 bits)
|
|
292
|
+
- Frame Offset (6 bits)
|
|
293
|
+
- Frame Number (4 bytes, little-endian)
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
data: EoE protocol data.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Parsed EoE data.
|
|
300
|
+
"""
|
|
301
|
+
if len(data) < 4:
|
|
302
|
+
return {"error": "EoE data too short"}
|
|
303
|
+
|
|
304
|
+
type_port = data[0]
|
|
305
|
+
flags = data[1]
|
|
306
|
+
frag_offset = int.from_bytes(data[2:4], "little")
|
|
307
|
+
|
|
308
|
+
frame_type = type_port & 0x0F # Lower 4 bits
|
|
309
|
+
port = (type_port >> 4) & 0x0F # Upper 4 bits
|
|
310
|
+
|
|
311
|
+
last_fragment = bool(flags & 0x01) # Bit 0
|
|
312
|
+
time_append = bool(flags & 0x02) # Bit 1
|
|
313
|
+
time_request = bool(flags & 0x04) # Bit 2
|
|
314
|
+
|
|
315
|
+
fragment_number = frag_offset & 0x3F # Lower 6 bits
|
|
316
|
+
frame_offset = (frag_offset >> 6) & 0x3F # Bits 6-11
|
|
317
|
+
|
|
318
|
+
# EoE frame types
|
|
319
|
+
type_names = {
|
|
320
|
+
0x00: "Fragment",
|
|
321
|
+
0x01: "Init Request",
|
|
322
|
+
0x02: "Init Response",
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
result: dict[str, Any] = {
|
|
326
|
+
"type": type_names.get(frame_type, f"Unknown (0x{frame_type:X})"),
|
|
327
|
+
"type_code": frame_type,
|
|
328
|
+
"port": port,
|
|
329
|
+
"last_fragment": last_fragment,
|
|
330
|
+
"time_append": time_append,
|
|
331
|
+
"time_request": time_request,
|
|
332
|
+
"fragment_number": fragment_number,
|
|
333
|
+
"frame_offset": frame_offset,
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return result
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
__all__ = ["parse_mailbox"]
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""EtherCAT topology discovery utilities.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for discovering and analyzing EtherCAT network
|
|
4
|
+
topology, including ring/line detection and slave ordering.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from oscura.analyzers.protocols.industrial.ethercat.topology import TopologyAnalyzer
|
|
8
|
+
>>> from oscura.analyzers.protocols.industrial.ethercat.analyzer import EtherCATAnalyzer
|
|
9
|
+
>>> analyzer = EtherCATAnalyzer()
|
|
10
|
+
>>> # Parse frames...
|
|
11
|
+
>>> topology = TopologyAnalyzer(analyzer)
|
|
12
|
+
>>> network_type = topology.detect_network_type()
|
|
13
|
+
>>> print(f"Network: {network_type}")
|
|
14
|
+
|
|
15
|
+
References:
|
|
16
|
+
ETG.1000 Section 5: Topology and Addressing
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from oscura.analyzers.protocols.industrial.ethercat.analyzer import (
|
|
26
|
+
EtherCATAnalyzer,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class TopologyInfo:
|
|
32
|
+
"""EtherCAT network topology information.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
network_type: Network topology type ("line", "ring", "star", "unknown").
|
|
36
|
+
slave_count: Number of discovered slaves.
|
|
37
|
+
slave_addresses: List of slave addresses in order.
|
|
38
|
+
open_ports: List of detected open ports (for line topology).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
network_type: str
|
|
42
|
+
slave_count: int
|
|
43
|
+
slave_addresses: list[int]
|
|
44
|
+
open_ports: list[int]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TopologyAnalyzer:
|
|
48
|
+
"""EtherCAT topology analyzer.
|
|
49
|
+
|
|
50
|
+
Analyzes EtherCAT frames to determine network topology and slave ordering.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
analyzer: EtherCAT analyzer instance.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> from oscura.analyzers.protocols.industrial.ethercat.analyzer import EtherCATAnalyzer
|
|
57
|
+
>>> analyzer = EtherCATAnalyzer()
|
|
58
|
+
>>> # Parse some frames...
|
|
59
|
+
>>> topology = TopologyAnalyzer(analyzer)
|
|
60
|
+
>>> info = topology.analyze()
|
|
61
|
+
>>> print(f"Topology: {info.network_type}, Slaves: {info.slave_count}")
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, analyzer: EtherCATAnalyzer) -> None:
|
|
65
|
+
"""Initialize topology analyzer.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
analyzer: EtherCAT analyzer with parsed frames.
|
|
69
|
+
"""
|
|
70
|
+
self.analyzer = analyzer
|
|
71
|
+
|
|
72
|
+
def detect_network_type(self) -> str:
|
|
73
|
+
"""Detect network topology type.
|
|
74
|
+
|
|
75
|
+
Analyzes port descriptor registers and working counters to determine
|
|
76
|
+
whether the network is configured as a line or ring topology.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Network type: "line", "ring", "star", or "unknown".
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
>>> topology = TopologyAnalyzer(analyzer)
|
|
83
|
+
>>> network_type = topology.detect_network_type()
|
|
84
|
+
>>> assert network_type in ["line", "ring", "star", "unknown"]
|
|
85
|
+
"""
|
|
86
|
+
# Analyze frames for topology indicators
|
|
87
|
+
open_ports = self._detect_open_ports()
|
|
88
|
+
|
|
89
|
+
if len(open_ports) == 0:
|
|
90
|
+
# All ports connected - likely ring topology
|
|
91
|
+
return "ring"
|
|
92
|
+
elif len(open_ports) == 2:
|
|
93
|
+
# Two open ports - line topology (first and last slave)
|
|
94
|
+
return "line"
|
|
95
|
+
elif len(open_ports) > 2:
|
|
96
|
+
# Multiple open ports - star or complex topology
|
|
97
|
+
return "star"
|
|
98
|
+
else:
|
|
99
|
+
return "unknown"
|
|
100
|
+
|
|
101
|
+
def _detect_open_ports(self) -> list[int]:
|
|
102
|
+
"""Detect open (unconnected) ports in the network.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
List of slave addresses with open ports.
|
|
106
|
+
"""
|
|
107
|
+
open_ports: list[int] = []
|
|
108
|
+
|
|
109
|
+
# Port descriptor register is at address 0x0007
|
|
110
|
+
# Open ports show as not connected in the port descriptor
|
|
111
|
+
for frame in self.analyzer.frames:
|
|
112
|
+
for datagram in frame.datagrams:
|
|
113
|
+
if datagram.ado == 0x0007: # Port descriptor register
|
|
114
|
+
if len(datagram.data) >= 1:
|
|
115
|
+
port_desc = datagram.data[0]
|
|
116
|
+
# Bits indicate port types: 0=not implemented, 1=not configured,
|
|
117
|
+
# 2=EBUS, 3=MII
|
|
118
|
+
# Open ports show as 0 or 1
|
|
119
|
+
if port_desc & 0x03 in {0, 1}: # Port 0 or 1 open
|
|
120
|
+
open_ports.append(datagram.adp)
|
|
121
|
+
|
|
122
|
+
return open_ports
|
|
123
|
+
|
|
124
|
+
def analyze(self) -> TopologyInfo:
|
|
125
|
+
"""Analyze complete topology information.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Complete topology information.
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
>>> topology = TopologyAnalyzer(analyzer)
|
|
132
|
+
>>> info = topology.analyze()
|
|
133
|
+
>>> print(f"Found {info.slave_count} slaves in {info.network_type} topology")
|
|
134
|
+
"""
|
|
135
|
+
network_type = self.detect_network_type()
|
|
136
|
+
slave_addresses = self.analyzer.discover_topology()
|
|
137
|
+
open_ports = self._detect_open_ports()
|
|
138
|
+
|
|
139
|
+
return TopologyInfo(
|
|
140
|
+
network_type=network_type,
|
|
141
|
+
slave_count=len(slave_addresses),
|
|
142
|
+
slave_addresses=slave_addresses,
|
|
143
|
+
open_ports=open_ports,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def get_slave_order(self) -> list[int]:
|
|
147
|
+
"""Get slaves in physical connection order.
|
|
148
|
+
|
|
149
|
+
For line topology, returns slaves ordered from master to end of chain.
|
|
150
|
+
For ring topology, returns slaves in ring order.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Ordered list of slave addresses.
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
>>> topology = TopologyAnalyzer(analyzer)
|
|
157
|
+
>>> order = topology.get_slave_order()
|
|
158
|
+
>>> print(f"Slave chain: {order}")
|
|
159
|
+
"""
|
|
160
|
+
# Use auto-increment addressing order as physical order
|
|
161
|
+
# Slaves are enumerated in the order they appear in the segment
|
|
162
|
+
slave_addresses = self.analyzer.discover_topology()
|
|
163
|
+
return slave_addresses
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
__all__ = ["TopologyAnalyzer", "TopologyInfo"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Modbus RTU/TCP protocol support.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive Modbus protocol analysis for both RTU (serial)
|
|
4
|
+
and TCP (Ethernet) variants, including all standard function codes, CRC validation,
|
|
5
|
+
and device state tracking.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.analyzers.protocols.industrial.modbus import ModbusAnalyzer
|
|
9
|
+
>>> analyzer = ModbusAnalyzer()
|
|
10
|
+
>>> message = analyzer.parse_rtu(frame_bytes)
|
|
11
|
+
>>> print(f"{message.function_name}: {message.parsed_data}")
|
|
12
|
+
|
|
13
|
+
References:
|
|
14
|
+
Modbus Application Protocol V1.1b3
|
|
15
|
+
Modbus over Serial Line V1.02
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from oscura.analyzers.protocols.industrial.modbus.analyzer import (
|
|
19
|
+
ModbusAnalyzer,
|
|
20
|
+
ModbusDevice,
|
|
21
|
+
ModbusMessage,
|
|
22
|
+
)
|
|
23
|
+
from oscura.analyzers.protocols.industrial.modbus.crc import calculate_crc, verify_crc
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"ModbusAnalyzer",
|
|
27
|
+
"ModbusDevice",
|
|
28
|
+
"ModbusMessage",
|
|
29
|
+
"calculate_crc",
|
|
30
|
+
"verify_crc",
|
|
31
|
+
]
|