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,474 @@
|
|
|
1
|
+
"""EtherCAT protocol analyzer.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive EtherCAT (Ethernet for Control Automation Technology)
|
|
4
|
+
protocol analysis supporting frame parsing, datagram decoding, topology discovery,
|
|
5
|
+
and slave configuration export.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.analyzers.protocols.industrial.ethercat.analyzer import EtherCATAnalyzer
|
|
9
|
+
>>> analyzer = EtherCATAnalyzer()
|
|
10
|
+
>>> # Parse EtherCAT frame (Ethertype 0x88A4)
|
|
11
|
+
>>> ethernet_payload = bytes([0x11, 0x10, 0x01, 0x00, ...])
|
|
12
|
+
>>> frame = analyzer.parse_frame(ethernet_payload, timestamp=0.0)
|
|
13
|
+
>>> print(f"Datagrams: {len(frame.datagrams)}")
|
|
14
|
+
>>> # Access slave information
|
|
15
|
+
>>> slave = analyzer.read_slave_info(station_address=1)
|
|
16
|
+
>>> if slave:
|
|
17
|
+
... print(f"State: {slave.state}")
|
|
18
|
+
|
|
19
|
+
References:
|
|
20
|
+
IEC 61158 Type 12: Industrial communication networks
|
|
21
|
+
ETG.1000 EtherCAT Protocol Specification
|
|
22
|
+
ETG.2000 EtherCAT AL Protocol (Application Layer)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import xml.etree.ElementTree as ET
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import ClassVar
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class EtherCATDatagram:
|
|
35
|
+
"""EtherCAT datagram within frame.
|
|
36
|
+
|
|
37
|
+
A single EtherCAT frame can contain multiple datagrams. Each datagram
|
|
38
|
+
represents a command (read/write) to EtherCAT slave devices.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
cmd: Command type code (0x00-0x0E).
|
|
42
|
+
cmd_name: Human-readable command name.
|
|
43
|
+
idx: Index field (lower 4 bits of first byte).
|
|
44
|
+
adp: Auto-increment address (2 bytes).
|
|
45
|
+
ado: Address offset (2 bytes).
|
|
46
|
+
len_: Data length (11 bits).
|
|
47
|
+
irq: Interrupt requested (4 bits).
|
|
48
|
+
data: Datagram payload data.
|
|
49
|
+
wkc: Working counter (2 bytes).
|
|
50
|
+
more_follows: True if more datagrams follow in frame.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
cmd: int
|
|
54
|
+
cmd_name: str
|
|
55
|
+
idx: int
|
|
56
|
+
adp: int
|
|
57
|
+
ado: int
|
|
58
|
+
len_: int
|
|
59
|
+
irq: int
|
|
60
|
+
data: bytes
|
|
61
|
+
wkc: int
|
|
62
|
+
more_follows: bool
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class EtherCATFrame:
|
|
67
|
+
"""EtherCAT frame representation.
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
timestamp: Frame timestamp in seconds.
|
|
71
|
+
length: Frame length field (11 bits).
|
|
72
|
+
datagrams: List of datagrams in frame.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
timestamp: float
|
|
76
|
+
length: int
|
|
77
|
+
datagrams: list[EtherCATDatagram]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class EtherCATSlave:
|
|
82
|
+
"""EtherCAT slave device.
|
|
83
|
+
|
|
84
|
+
Attributes:
|
|
85
|
+
station_address: Configured station address.
|
|
86
|
+
alias_address: Alias address (if configured).
|
|
87
|
+
vendor_id: Vendor identification.
|
|
88
|
+
product_code: Product code.
|
|
89
|
+
revision: Revision number.
|
|
90
|
+
serial_number: Serial number.
|
|
91
|
+
state: Current slave state (INIT, PRE-OP, SAFE-OP, OP).
|
|
92
|
+
dc_supported: Distributed Clock support.
|
|
93
|
+
mailbox_protocols: Supported mailbox protocols (CoE, FoE, SoE, EoE).
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
station_address: int
|
|
97
|
+
alias_address: int | None = None
|
|
98
|
+
vendor_id: int | None = None
|
|
99
|
+
product_code: int | None = None
|
|
100
|
+
revision: int | None = None
|
|
101
|
+
serial_number: int | None = None
|
|
102
|
+
state: str = "UNKNOWN"
|
|
103
|
+
dc_supported: bool = False
|
|
104
|
+
mailbox_protocols: list[str] = field(default_factory=list)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class EtherCATAnalyzer:
|
|
108
|
+
"""EtherCAT protocol analyzer.
|
|
109
|
+
|
|
110
|
+
Provides comprehensive EtherCAT protocol analysis including frame parsing,
|
|
111
|
+
datagram decoding, topology discovery, and slave state tracking.
|
|
112
|
+
|
|
113
|
+
Attributes:
|
|
114
|
+
frames: List of parsed EtherCAT frames.
|
|
115
|
+
slaves: Dictionary of discovered slaves by station address.
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
>>> analyzer = EtherCATAnalyzer()
|
|
119
|
+
>>> # Parse frame
|
|
120
|
+
>>> frame = analyzer.parse_frame(ethernet_payload, timestamp=0.0)
|
|
121
|
+
>>> # Discover topology
|
|
122
|
+
>>> slave_addresses = analyzer.discover_topology()
|
|
123
|
+
>>> # Export configuration
|
|
124
|
+
>>> from pathlib import Path
|
|
125
|
+
>>> analyzer.export_configuration(Path("ethercat_config.xml"))
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
# Command types (EtherCAT datagram commands)
|
|
129
|
+
COMMANDS: ClassVar[dict[int, str]] = {
|
|
130
|
+
0x00: "NOP",
|
|
131
|
+
0x01: "APRD", # Auto-increment Physical Read
|
|
132
|
+
0x02: "APWR", # Auto-increment Physical Write
|
|
133
|
+
0x03: "APRW", # Auto-increment Physical Read/Write
|
|
134
|
+
0x04: "FPRD", # Configured address Physical Read
|
|
135
|
+
0x05: "FPWR", # Configured address Physical Write
|
|
136
|
+
0x06: "FPRW", # Configured address Physical Read/Write
|
|
137
|
+
0x07: "BRD", # Broadcast Read
|
|
138
|
+
0x08: "BWR", # Broadcast Write
|
|
139
|
+
0x09: "BRW", # Broadcast Read/Write
|
|
140
|
+
0x0A: "LRD", # Logical Memory Read
|
|
141
|
+
0x0B: "LWR", # Logical Memory Write
|
|
142
|
+
0x0C: "LRW", # Logical Memory Read/Write
|
|
143
|
+
0x0D: "ARMW", # Auto-increment physical Read Multiple Write
|
|
144
|
+
0x0E: "FRMW", # Configured address physical Read Multiple Write
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Slave state machine states
|
|
148
|
+
STATES: ClassVar[dict[int, str]] = {
|
|
149
|
+
0x01: "INIT",
|
|
150
|
+
0x02: "PRE-OP",
|
|
151
|
+
0x04: "SAFE-OP",
|
|
152
|
+
0x08: "OP",
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Mailbox protocol types
|
|
156
|
+
MAILBOX_PROTOCOLS: ClassVar[dict[int, str]] = {
|
|
157
|
+
0x02: "CoE", # CAN application protocol over EtherCAT
|
|
158
|
+
0x03: "FoE", # File access over EtherCAT
|
|
159
|
+
0x04: "SoE", # Servo drive profile over EtherCAT
|
|
160
|
+
0x05: "EoE", # Ethernet over EtherCAT
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Well-known EtherCAT register addresses
|
|
164
|
+
REG_TYPE: ClassVar[int] = 0x0000 # Type
|
|
165
|
+
REG_REVISION: ClassVar[int] = 0x0001 # Revision
|
|
166
|
+
REG_BUILD: ClassVar[int] = 0x0002 # Build
|
|
167
|
+
REG_FMMU_COUNT: ClassVar[int] = 0x0004 # FMMU count
|
|
168
|
+
REG_SYNC_COUNT: ClassVar[int] = 0x0005 # SyncManager count
|
|
169
|
+
REG_PORT_DESC: ClassVar[int] = 0x0007 # Port descriptor
|
|
170
|
+
REG_ESC_FEATURES: ClassVar[int] = 0x0008 # ESC features
|
|
171
|
+
REG_STATION_ADDRESS: ClassVar[int] = 0x0010 # Configured Station Address
|
|
172
|
+
REG_ALIAS_ADDRESS: ClassVar[int] = 0x0012 # Alias Address
|
|
173
|
+
REG_DL_CONTROL: ClassVar[int] = 0x0100 # DL Control
|
|
174
|
+
REG_DL_STATUS: ClassVar[int] = 0x0110 # DL Status
|
|
175
|
+
REG_AL_CONTROL: ClassVar[int] = 0x0120 # AL Control
|
|
176
|
+
REG_AL_STATUS: ClassVar[int] = 0x0130 # AL Status
|
|
177
|
+
REG_AL_STATUS_CODE: ClassVar[int] = 0x0134 # AL Status Code
|
|
178
|
+
REG_DC_SYSTEM_TIME: ClassVar[int] = 0x0910 # Distributed Clock System Time
|
|
179
|
+
|
|
180
|
+
def __init__(self) -> None:
|
|
181
|
+
"""Initialize EtherCAT analyzer."""
|
|
182
|
+
self.frames: list[EtherCATFrame] = []
|
|
183
|
+
self.slaves: dict[int, EtherCATSlave] = {}
|
|
184
|
+
|
|
185
|
+
def parse_frame(self, ethernet_payload: bytes, timestamp: float = 0.0) -> EtherCATFrame:
|
|
186
|
+
"""Parse EtherCAT frame (Ethertype 0x88A4).
|
|
187
|
+
|
|
188
|
+
Frame Format:
|
|
189
|
+
- Length (2 bytes) - lower 11 bits, upper 5 bits reserved
|
|
190
|
+
- Type (1 byte) - Protocol type (0x01 for EtherCAT commands)
|
|
191
|
+
- Datagrams (variable) - One or more datagrams
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
ethernet_payload: EtherCAT frame payload (after Ethernet header).
|
|
195
|
+
timestamp: Frame timestamp in seconds.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Parsed EtherCAT frame.
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
ValueError: If frame is invalid.
|
|
202
|
+
|
|
203
|
+
Example:
|
|
204
|
+
>>> analyzer = EtherCATAnalyzer()
|
|
205
|
+
>>> # Simple frame with one datagram
|
|
206
|
+
>>> payload = bytes([0x0E, 0x10, 0x01, 0x00, 0x00, 0x00, ...])
|
|
207
|
+
>>> frame = analyzer.parse_frame(payload, timestamp=0.0)
|
|
208
|
+
>>> assert len(frame.datagrams) >= 1
|
|
209
|
+
"""
|
|
210
|
+
if len(ethernet_payload) < 2:
|
|
211
|
+
raise ValueError(f"EtherCAT frame too short: {len(ethernet_payload)} bytes (minimum 2)")
|
|
212
|
+
|
|
213
|
+
# Parse length field (11 bits)
|
|
214
|
+
length_and_reserved = int.from_bytes(ethernet_payload[0:2], "little")
|
|
215
|
+
length = length_and_reserved & 0x07FF
|
|
216
|
+
|
|
217
|
+
datagrams: list[EtherCATDatagram] = []
|
|
218
|
+
offset = 2
|
|
219
|
+
|
|
220
|
+
# Parse all datagrams
|
|
221
|
+
while offset < len(ethernet_payload):
|
|
222
|
+
try:
|
|
223
|
+
datagram, consumed = self._parse_datagram(ethernet_payload, offset)
|
|
224
|
+
datagrams.append(datagram)
|
|
225
|
+
offset += consumed
|
|
226
|
+
|
|
227
|
+
if not datagram.more_follows:
|
|
228
|
+
break
|
|
229
|
+
except ValueError:
|
|
230
|
+
# If we fail to parse a datagram but have already parsed some, stop gracefully
|
|
231
|
+
if len(datagrams) > 0:
|
|
232
|
+
break
|
|
233
|
+
raise
|
|
234
|
+
|
|
235
|
+
frame = EtherCATFrame(
|
|
236
|
+
timestamp=timestamp,
|
|
237
|
+
length=length,
|
|
238
|
+
datagrams=datagrams,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
self.frames.append(frame)
|
|
242
|
+
self._update_slave_state(frame)
|
|
243
|
+
|
|
244
|
+
return frame
|
|
245
|
+
|
|
246
|
+
def _parse_datagram(self, data: bytes, offset: int) -> tuple[EtherCATDatagram, int]:
|
|
247
|
+
"""Parse single EtherCAT datagram.
|
|
248
|
+
|
|
249
|
+
Datagram Format:
|
|
250
|
+
- Cmd (1 byte) - Command type
|
|
251
|
+
- Idx (1 byte) - Index
|
|
252
|
+
- ADP (2 bytes) - Auto-increment address (little-endian)
|
|
253
|
+
- ADO (2 bytes) - Address offset (little-endian)
|
|
254
|
+
- Len/M/IRQ (2 bytes) - Length (11 bits), More (1 bit), IRQ (4 bits)
|
|
255
|
+
- Data (Len bytes)
|
|
256
|
+
- WKC (2 bytes) - Working counter (little-endian)
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
data: Frame data containing datagram.
|
|
260
|
+
offset: Offset to start of datagram.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Tuple of (datagram, bytes_consumed).
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
ValueError: If datagram is invalid.
|
|
267
|
+
"""
|
|
268
|
+
if offset + 10 > len(data):
|
|
269
|
+
raise ValueError(
|
|
270
|
+
f"Insufficient data for datagram header at offset {offset}: "
|
|
271
|
+
f"need 10 bytes, have {len(data) - offset}"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
cmd = data[offset]
|
|
275
|
+
idx = data[offset + 1]
|
|
276
|
+
adp = int.from_bytes(data[offset + 2 : offset + 4], "little")
|
|
277
|
+
ado = int.from_bytes(data[offset + 4 : offset + 6], "little")
|
|
278
|
+
len_m_irq = int.from_bytes(data[offset + 6 : offset + 8], "little")
|
|
279
|
+
|
|
280
|
+
# Extract fields from len_m_irq
|
|
281
|
+
data_len = len_m_irq & 0x07FF # Lower 11 bits
|
|
282
|
+
more_follows = bool(len_m_irq & 0x8000) # Bit 15
|
|
283
|
+
irq = (len_m_irq >> 12) & 0x0F # Bits 12-15 (but bit 15 is M flag)
|
|
284
|
+
|
|
285
|
+
# Validate data length
|
|
286
|
+
if offset + 10 + data_len > len(data):
|
|
287
|
+
raise ValueError(
|
|
288
|
+
f"Insufficient data for datagram payload at offset {offset}: "
|
|
289
|
+
f"need {data_len} bytes, have {len(data) - offset - 10}"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
datagram_data = data[offset + 8 : offset + 8 + data_len]
|
|
293
|
+
wkc = int.from_bytes(data[offset + 8 + data_len : offset + 10 + data_len], "little")
|
|
294
|
+
|
|
295
|
+
total_consumed = 10 + data_len
|
|
296
|
+
|
|
297
|
+
datagram = EtherCATDatagram(
|
|
298
|
+
cmd=cmd,
|
|
299
|
+
cmd_name=self.COMMANDS.get(cmd, f"Unknown (0x{cmd:02X})"),
|
|
300
|
+
idx=idx,
|
|
301
|
+
adp=adp,
|
|
302
|
+
ado=ado,
|
|
303
|
+
len_=data_len,
|
|
304
|
+
irq=irq,
|
|
305
|
+
data=datagram_data,
|
|
306
|
+
wkc=wkc,
|
|
307
|
+
more_follows=more_follows,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return datagram, total_consumed
|
|
311
|
+
|
|
312
|
+
def _update_slave_state(self, frame: EtherCATFrame) -> None:
|
|
313
|
+
"""Update slave state based on parsed frame.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
frame: Parsed EtherCAT frame.
|
|
317
|
+
"""
|
|
318
|
+
for datagram in frame.datagrams:
|
|
319
|
+
# Check for AL Status register reads (address 0x0130)
|
|
320
|
+
if datagram.ado == self.REG_AL_STATUS and len(datagram.data) >= 2:
|
|
321
|
+
state_code = datagram.data[0] & 0x0F # Lower 4 bits
|
|
322
|
+
state_name = self.STATES.get(state_code, "UNKNOWN")
|
|
323
|
+
|
|
324
|
+
# For auto-increment addressing, adp indicates position
|
|
325
|
+
if datagram.cmd in {0x01, 0x02, 0x03}: # APRD, APWR, APRW
|
|
326
|
+
station_address = datagram.adp
|
|
327
|
+
if station_address not in self.slaves:
|
|
328
|
+
self.slaves[station_address] = EtherCATSlave(
|
|
329
|
+
station_address=station_address
|
|
330
|
+
)
|
|
331
|
+
self.slaves[station_address].state = state_name
|
|
332
|
+
|
|
333
|
+
# Check for configured station address reads (address 0x0010)
|
|
334
|
+
if datagram.ado == self.REG_STATION_ADDRESS and len(datagram.data) >= 2:
|
|
335
|
+
station_address = int.from_bytes(datagram.data[0:2], "little")
|
|
336
|
+
if station_address not in self.slaves and station_address != 0:
|
|
337
|
+
self.slaves[station_address] = EtherCATSlave(station_address=station_address)
|
|
338
|
+
|
|
339
|
+
def discover_topology(self) -> list[int]:
|
|
340
|
+
"""Discover slave topology using auto-increment addressing.
|
|
341
|
+
|
|
342
|
+
Uses the auto-increment addressing mechanism to enumerate all slaves
|
|
343
|
+
in the EtherCAT segment. This method analyzes already-parsed frames
|
|
344
|
+
to extract slave addresses.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
List of discovered slave station addresses.
|
|
348
|
+
|
|
349
|
+
Example:
|
|
350
|
+
>>> analyzer = EtherCATAnalyzer()
|
|
351
|
+
>>> # Parse some frames first...
|
|
352
|
+
>>> addresses = analyzer.discover_topology()
|
|
353
|
+
>>> print(f"Found {len(addresses)} slaves")
|
|
354
|
+
"""
|
|
355
|
+
discovered: set[int] = set()
|
|
356
|
+
|
|
357
|
+
for frame in self.frames:
|
|
358
|
+
for datagram in frame.datagrams:
|
|
359
|
+
# Auto-increment commands (APRD, APWR, APRW)
|
|
360
|
+
if datagram.cmd in {0x01, 0x02, 0x03}:
|
|
361
|
+
# ADP field is the position, WKC indicates success
|
|
362
|
+
if datagram.wkc > 0:
|
|
363
|
+
discovered.add(datagram.adp)
|
|
364
|
+
|
|
365
|
+
# Configured address reads
|
|
366
|
+
elif datagram.cmd in {0x04, 0x05, 0x06}: # FPRD, FPWR, FPRW
|
|
367
|
+
if datagram.wkc > 0:
|
|
368
|
+
discovered.add(datagram.adp)
|
|
369
|
+
|
|
370
|
+
return sorted(discovered)
|
|
371
|
+
|
|
372
|
+
def read_slave_info(self, station_address: int) -> EtherCATSlave | None:
|
|
373
|
+
"""Read slave information from analysis.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
station_address: Slave station address.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Slave information if found, None otherwise.
|
|
380
|
+
|
|
381
|
+
Example:
|
|
382
|
+
>>> analyzer = EtherCATAnalyzer()
|
|
383
|
+
>>> # After parsing frames...
|
|
384
|
+
>>> slave = analyzer.read_slave_info(station_address=1)
|
|
385
|
+
>>> if slave:
|
|
386
|
+
... print(f"State: {slave.state}")
|
|
387
|
+
"""
|
|
388
|
+
return self.slaves.get(station_address)
|
|
389
|
+
|
|
390
|
+
def export_configuration(self, output_path: Path) -> None:
|
|
391
|
+
"""Export slave configuration as ENI (EtherCAT Network Information) XML.
|
|
392
|
+
|
|
393
|
+
Creates a simplified ENI XML file containing discovered slave configuration.
|
|
394
|
+
This is compatible with EtherCAT master implementations.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
output_path: Path to output XML file.
|
|
398
|
+
|
|
399
|
+
Example:
|
|
400
|
+
>>> analyzer = EtherCATAnalyzer()
|
|
401
|
+
>>> # After parsing frames...
|
|
402
|
+
>>> from pathlib import Path
|
|
403
|
+
>>> analyzer.export_configuration(Path("ethercat_network.xml"))
|
|
404
|
+
"""
|
|
405
|
+
# Create root element
|
|
406
|
+
root = ET.Element("EtherCATConfig")
|
|
407
|
+
root.set("Version", "1.0")
|
|
408
|
+
|
|
409
|
+
# Create Config section
|
|
410
|
+
config = ET.SubElement(root, "Config")
|
|
411
|
+
|
|
412
|
+
# Create Master section
|
|
413
|
+
master = ET.SubElement(config, "Master")
|
|
414
|
+
|
|
415
|
+
# Add slaves
|
|
416
|
+
for slave in sorted(self.slaves.values(), key=lambda s: s.station_address):
|
|
417
|
+
slave_elem = ET.SubElement(master, "Slave")
|
|
418
|
+
|
|
419
|
+
# Add slave information
|
|
420
|
+
info = ET.SubElement(slave_elem, "Info")
|
|
421
|
+
|
|
422
|
+
station = ET.SubElement(info, "StationAddress")
|
|
423
|
+
station.text = str(slave.station_address)
|
|
424
|
+
|
|
425
|
+
if slave.alias_address is not None:
|
|
426
|
+
alias = ET.SubElement(info, "AliasAddress")
|
|
427
|
+
alias.text = str(slave.alias_address)
|
|
428
|
+
|
|
429
|
+
if slave.vendor_id is not None:
|
|
430
|
+
vendor = ET.SubElement(info, "VendorId")
|
|
431
|
+
vendor.text = f"0x{slave.vendor_id:08X}"
|
|
432
|
+
|
|
433
|
+
if slave.product_code is not None:
|
|
434
|
+
product = ET.SubElement(info, "ProductCode")
|
|
435
|
+
product.text = f"0x{slave.product_code:08X}"
|
|
436
|
+
|
|
437
|
+
if slave.revision is not None:
|
|
438
|
+
revision = ET.SubElement(info, "Revision")
|
|
439
|
+
revision.text = f"0x{slave.revision:08X}"
|
|
440
|
+
|
|
441
|
+
if slave.serial_number is not None:
|
|
442
|
+
serial = ET.SubElement(info, "SerialNumber")
|
|
443
|
+
serial.text = str(slave.serial_number)
|
|
444
|
+
|
|
445
|
+
# Add state information
|
|
446
|
+
state = ET.SubElement(info, "State")
|
|
447
|
+
state.text = slave.state
|
|
448
|
+
|
|
449
|
+
# Add DC support
|
|
450
|
+
if slave.dc_supported:
|
|
451
|
+
dc = ET.SubElement(info, "Dc")
|
|
452
|
+
dc.set("supported", "true")
|
|
453
|
+
|
|
454
|
+
# Add mailbox protocols
|
|
455
|
+
if slave.mailbox_protocols:
|
|
456
|
+
mailbox = ET.SubElement(info, "Mailbox")
|
|
457
|
+
for protocol in slave.mailbox_protocols:
|
|
458
|
+
proto_elem = ET.SubElement(mailbox, "Protocol")
|
|
459
|
+
proto_elem.text = protocol
|
|
460
|
+
|
|
461
|
+
# Create ElementTree and write to file
|
|
462
|
+
tree = ET.ElementTree(root)
|
|
463
|
+
ET.indent(tree, space=" ")
|
|
464
|
+
|
|
465
|
+
with output_path.open("wb") as f:
|
|
466
|
+
tree.write(f, encoding="utf-8", xml_declaration=True)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
__all__ = [
|
|
470
|
+
"EtherCATAnalyzer",
|
|
471
|
+
"EtherCATDatagram",
|
|
472
|
+
"EtherCATFrame",
|
|
473
|
+
"EtherCATSlave",
|
|
474
|
+
]
|