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,668 @@
|
|
|
1
|
+
"""CoAP protocol analyzer with RFC 7252 support and extensions.
|
|
2
|
+
|
|
3
|
+
Provides comprehensive CoAP message parsing, request/response matching,
|
|
4
|
+
blockwise transfer support (RFC 7959), and observe extension (RFC 7641).
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from oscura.iot.coap import CoAPAnalyzer
|
|
8
|
+
>>> analyzer = CoAPAnalyzer()
|
|
9
|
+
>>> data = bytes([0x40, 0x01, 0x12, 0x34]) # CON GET
|
|
10
|
+
>>> message = analyzer.parse_message(data, timestamp=0.0)
|
|
11
|
+
>>> print(message.msg_type, message.code)
|
|
12
|
+
CON GET
|
|
13
|
+
|
|
14
|
+
References:
|
|
15
|
+
RFC 7252: CoAP (Constrained Application Protocol)
|
|
16
|
+
RFC 7959: Block-Wise Transfers in CoAP
|
|
17
|
+
RFC 7641: Observing Resources in CoAP
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, ClassVar
|
|
26
|
+
|
|
27
|
+
from oscura.iot.coap.options import (
|
|
28
|
+
CONTENT_FORMATS,
|
|
29
|
+
OPTIONS,
|
|
30
|
+
OptionParser,
|
|
31
|
+
format_block_option,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class CoAPMessage:
|
|
37
|
+
"""CoAP message representation.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
timestamp: Message timestamp in seconds.
|
|
41
|
+
version: CoAP version (always 1 for RFC 7252).
|
|
42
|
+
msg_type: Message type ("CON", "NON", "ACK", "RST").
|
|
43
|
+
code: Method code or response code (e.g., "GET", "2.05 Content").
|
|
44
|
+
message_id: 16-bit message identifier for duplicate detection.
|
|
45
|
+
token: Token for matching requests/responses (0-8 bytes).
|
|
46
|
+
options: Parsed options dictionary (option name -> list of values).
|
|
47
|
+
payload: Message payload bytes.
|
|
48
|
+
is_request: True if request, False if response.
|
|
49
|
+
uri: Reconstructed URI from Uri-* options (optional).
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
>>> msg = CoAPMessage(
|
|
53
|
+
... timestamp=1.0,
|
|
54
|
+
... version=1,
|
|
55
|
+
... msg_type="CON",
|
|
56
|
+
... code="GET",
|
|
57
|
+
... message_id=0x1234,
|
|
58
|
+
... token=b"\\x01",
|
|
59
|
+
... )
|
|
60
|
+
>>> msg.is_request
|
|
61
|
+
True
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
timestamp: float
|
|
65
|
+
version: int
|
|
66
|
+
msg_type: str
|
|
67
|
+
code: str
|
|
68
|
+
message_id: int
|
|
69
|
+
token: bytes
|
|
70
|
+
options: dict[str, list[Any]] = field(default_factory=dict)
|
|
71
|
+
payload: bytes = b""
|
|
72
|
+
is_request: bool = True
|
|
73
|
+
uri: str | None = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class CoAPExchange:
|
|
78
|
+
"""CoAP request-response exchange.
|
|
79
|
+
|
|
80
|
+
Represents a complete request-response transaction, including
|
|
81
|
+
support for multiple responses (observe pattern).
|
|
82
|
+
|
|
83
|
+
Attributes:
|
|
84
|
+
request: Initial CoAP request message.
|
|
85
|
+
responses: List of response messages (multiple for observe).
|
|
86
|
+
complete: True if exchange is complete (no more responses expected).
|
|
87
|
+
observe: True if this is an observe relationship.
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
>>> request = CoAPMessage(...)
|
|
91
|
+
>>> exchange = CoAPExchange(request=request)
|
|
92
|
+
>>> exchange.responses.append(response_msg)
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
request: CoAPMessage
|
|
96
|
+
responses: list[CoAPMessage] = field(default_factory=list)
|
|
97
|
+
complete: bool = False
|
|
98
|
+
observe: bool = False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class CoAPAnalyzer:
|
|
102
|
+
"""CoAP protocol analyzer supporting RFC 7252 and extensions.
|
|
103
|
+
|
|
104
|
+
Analyzes CoAP messages including parsing message format, decoding options,
|
|
105
|
+
matching requests with responses, and handling blockwise transfers and
|
|
106
|
+
observe relationships.
|
|
107
|
+
|
|
108
|
+
Attributes:
|
|
109
|
+
MSG_TYPES: Message type code to name mapping.
|
|
110
|
+
METHODS: Method code to name mapping.
|
|
111
|
+
RESPONSE_CLASSES: Response class descriptions.
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
>>> analyzer = CoAPAnalyzer()
|
|
115
|
+
>>> msg = analyzer.parse_message(data, timestamp=0.0)
|
|
116
|
+
>>> analyzer.match_request_response()
|
|
117
|
+
>>> analyzer.export_exchanges(Path("coap_traffic.json"))
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
# Message types (RFC 7252 Section 3)
|
|
121
|
+
MSG_TYPES: ClassVar[dict[int, str]] = {
|
|
122
|
+
0: "CON", # Confirmable
|
|
123
|
+
1: "NON", # Non-confirmable
|
|
124
|
+
2: "ACK", # Acknowledgement
|
|
125
|
+
3: "RST", # Reset
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# Method codes 0.xx (RFC 7252 Section 5.8)
|
|
129
|
+
METHODS: ClassVar[dict[int, str]] = {
|
|
130
|
+
1: "GET",
|
|
131
|
+
2: "POST",
|
|
132
|
+
3: "PUT",
|
|
133
|
+
4: "DELETE",
|
|
134
|
+
5: "FETCH", # RFC 8132
|
|
135
|
+
6: "PATCH", # RFC 8132
|
|
136
|
+
7: "iPATCH", # RFC 8132
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Response code classes
|
|
140
|
+
RESPONSE_CLASSES: ClassVar[dict[int, str]] = {
|
|
141
|
+
2: "Success",
|
|
142
|
+
4: "Client Error",
|
|
143
|
+
5: "Server Error",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def __init__(self) -> None:
|
|
147
|
+
"""Initialize CoAP analyzer.
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
>>> analyzer = CoAPAnalyzer()
|
|
151
|
+
>>> len(analyzer.messages)
|
|
152
|
+
0
|
|
153
|
+
"""
|
|
154
|
+
self.messages: list[CoAPMessage] = []
|
|
155
|
+
self.exchanges: dict[bytes, CoAPExchange] = {} # Token -> exchange
|
|
156
|
+
self.message_id_map: dict[int, CoAPMessage] = {} # Message ID -> message
|
|
157
|
+
self.option_parser = OptionParser()
|
|
158
|
+
|
|
159
|
+
def parse_message(self, data: bytes, timestamp: float = 0.0) -> CoAPMessage:
|
|
160
|
+
"""Parse CoAP message from bytes.
|
|
161
|
+
|
|
162
|
+
Parses complete CoAP message including header, token, options, and payload
|
|
163
|
+
according to RFC 7252 Section 3.
|
|
164
|
+
|
|
165
|
+
Message Format:
|
|
166
|
+
0 1 2 3
|
|
167
|
+
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
168
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
169
|
+
|Ver| T | TKL | Code | Message ID |
|
|
170
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
171
|
+
| Token (if any, TKL bytes) ...
|
|
172
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
173
|
+
| Options (if any) ...
|
|
174
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
175
|
+
|1 1 1 1 1 1 1 1| Payload (if any) ...
|
|
176
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
data: Raw CoAP message bytes.
|
|
180
|
+
timestamp: Message timestamp in seconds.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Parsed CoAP message.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
ValueError: If message is malformed or too short.
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
>>> analyzer = CoAPAnalyzer()
|
|
190
|
+
>>> data = bytes([0x40, 0x01, 0x12, 0x34]) # CON GET
|
|
191
|
+
>>> msg = analyzer.parse_message(data)
|
|
192
|
+
>>> msg.msg_type
|
|
193
|
+
'CON'
|
|
194
|
+
>>> msg.code
|
|
195
|
+
'GET'
|
|
196
|
+
"""
|
|
197
|
+
if len(data) < 4:
|
|
198
|
+
raise ValueError(f"CoAP message too short: {len(data)} bytes (minimum 4)")
|
|
199
|
+
|
|
200
|
+
# Parse header and token
|
|
201
|
+
version, msg_type, code_str, is_request, message_id, token, offset = (
|
|
202
|
+
self._parse_header_and_token(data)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Parse options
|
|
206
|
+
options = self._parse_options(data, offset)
|
|
207
|
+
|
|
208
|
+
# Find payload boundary
|
|
209
|
+
payload_start = self._find_payload_start(data, offset)
|
|
210
|
+
|
|
211
|
+
# Extract payload
|
|
212
|
+
payload = data[payload_start:] if payload_start < len(data) else b""
|
|
213
|
+
|
|
214
|
+
# Reconstruct URI from options
|
|
215
|
+
uri = self._reconstruct_uri(options)
|
|
216
|
+
|
|
217
|
+
message = CoAPMessage(
|
|
218
|
+
timestamp=timestamp,
|
|
219
|
+
version=version,
|
|
220
|
+
msg_type=msg_type,
|
|
221
|
+
code=code_str,
|
|
222
|
+
message_id=message_id,
|
|
223
|
+
token=token,
|
|
224
|
+
options=options,
|
|
225
|
+
payload=payload,
|
|
226
|
+
is_request=is_request,
|
|
227
|
+
uri=uri,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
self.messages.append(message)
|
|
231
|
+
self.message_id_map[message_id] = message
|
|
232
|
+
|
|
233
|
+
return message
|
|
234
|
+
|
|
235
|
+
def _parse_header_and_token(self, data: bytes) -> tuple[int, str, str, bool, int, bytes, int]:
|
|
236
|
+
"""Parse CoAP header and token fields.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
data: Raw message bytes.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Tuple of (version, msg_type, code_str, is_request, message_id, token, offset).
|
|
243
|
+
|
|
244
|
+
Raises:
|
|
245
|
+
ValueError: If header or token is invalid.
|
|
246
|
+
"""
|
|
247
|
+
byte0 = data[0]
|
|
248
|
+
version = (byte0 >> 6) & 0x03
|
|
249
|
+
msg_type_val = (byte0 >> 4) & 0x03
|
|
250
|
+
tkl = byte0 & 0x0F
|
|
251
|
+
|
|
252
|
+
if version != 1:
|
|
253
|
+
raise ValueError(f"Unsupported CoAP version: {version} (expected 1)")
|
|
254
|
+
|
|
255
|
+
if tkl > 8:
|
|
256
|
+
raise ValueError(f"Invalid token length: {tkl} (maximum 8)")
|
|
257
|
+
|
|
258
|
+
code_byte = data[1]
|
|
259
|
+
message_id = int.from_bytes(data[2:4], "big")
|
|
260
|
+
|
|
261
|
+
msg_type = self.MSG_TYPES.get(msg_type_val, f"UNKNOWN({msg_type_val})")
|
|
262
|
+
code_str, is_request = self._parse_code(code_byte)
|
|
263
|
+
|
|
264
|
+
offset = 4
|
|
265
|
+
|
|
266
|
+
# Parse token (TKL bytes)
|
|
267
|
+
token = b""
|
|
268
|
+
if tkl > 0:
|
|
269
|
+
if len(data) < offset + tkl:
|
|
270
|
+
raise ValueError(f"Insufficient data for token: need {tkl} bytes")
|
|
271
|
+
token = data[offset : offset + tkl]
|
|
272
|
+
offset += tkl
|
|
273
|
+
|
|
274
|
+
return version, msg_type, code_str, is_request, message_id, token, offset
|
|
275
|
+
|
|
276
|
+
def _find_payload_start(self, data: bytes, start_offset: int) -> int:
|
|
277
|
+
"""Find start of payload after options.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
data: Complete message data.
|
|
281
|
+
start_offset: Offset where options start.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Offset where payload starts.
|
|
285
|
+
|
|
286
|
+
Raises:
|
|
287
|
+
ValueError: If option encoding is invalid.
|
|
288
|
+
"""
|
|
289
|
+
payload_start = start_offset
|
|
290
|
+
while payload_start < len(data):
|
|
291
|
+
if data[payload_start] == 0xFF:
|
|
292
|
+
payload_start += 1 # Skip marker
|
|
293
|
+
break
|
|
294
|
+
if payload_start >= len(data):
|
|
295
|
+
break
|
|
296
|
+
|
|
297
|
+
option_byte = data[payload_start]
|
|
298
|
+
delta = (option_byte >> 4) & 0x0F
|
|
299
|
+
length = option_byte & 0x0F
|
|
300
|
+
|
|
301
|
+
payload_start += 1
|
|
302
|
+
|
|
303
|
+
# Handle extended delta
|
|
304
|
+
if delta == 13:
|
|
305
|
+
payload_start += 1
|
|
306
|
+
elif delta == 14:
|
|
307
|
+
payload_start += 2
|
|
308
|
+
elif delta == 15:
|
|
309
|
+
break # Payload marker
|
|
310
|
+
|
|
311
|
+
# Handle extended length
|
|
312
|
+
if length == 13:
|
|
313
|
+
payload_start += 1
|
|
314
|
+
elif length == 14:
|
|
315
|
+
payload_start += 2
|
|
316
|
+
elif length == 15:
|
|
317
|
+
raise ValueError("Invalid option length encoding (15)")
|
|
318
|
+
|
|
319
|
+
# Skip option value
|
|
320
|
+
if delta < 15:
|
|
321
|
+
actual_length = self._calculate_option_length(data, payload_start, length)
|
|
322
|
+
payload_start += actual_length
|
|
323
|
+
|
|
324
|
+
return payload_start
|
|
325
|
+
|
|
326
|
+
def _calculate_option_length(self, data: bytes, offset: int, length_base: int) -> int:
|
|
327
|
+
"""Calculate actual option length from extended encoding.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
data: Message data.
|
|
331
|
+
offset: Current offset (after length encoding bytes).
|
|
332
|
+
length_base: Base length value from option byte.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Actual option length in bytes.
|
|
336
|
+
"""
|
|
337
|
+
if length_base < 13:
|
|
338
|
+
return length_base
|
|
339
|
+
elif length_base == 13 and offset <= len(data):
|
|
340
|
+
return data[offset - 1] + 13
|
|
341
|
+
elif length_base == 14 and offset + 1 <= len(data):
|
|
342
|
+
return int.from_bytes(data[offset - 2 : offset], "big") + 269
|
|
343
|
+
return 0
|
|
344
|
+
|
|
345
|
+
def _parse_code(self, code: int) -> tuple[str, bool]:
|
|
346
|
+
"""Parse code byte into human-readable string and request flag.
|
|
347
|
+
|
|
348
|
+
Code byte format: class (3 bits) . detail (5 bits)
|
|
349
|
+
- 0.xx: Request methods
|
|
350
|
+
- 2.xx: Success responses
|
|
351
|
+
- 4.xx: Client error responses
|
|
352
|
+
- 5.xx: Server error responses
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
code: Code byte value (0-255).
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Tuple of (code_string, is_request).
|
|
359
|
+
|
|
360
|
+
Example:
|
|
361
|
+
>>> analyzer = CoAPAnalyzer()
|
|
362
|
+
>>> analyzer._parse_code(0x01)
|
|
363
|
+
('GET', True)
|
|
364
|
+
>>> analyzer._parse_code(0x45)
|
|
365
|
+
('2.05 Content', False)
|
|
366
|
+
"""
|
|
367
|
+
code_class = (code >> 5) & 0x07
|
|
368
|
+
code_detail = code & 0x1F
|
|
369
|
+
|
|
370
|
+
if code_class == 0:
|
|
371
|
+
# Request method
|
|
372
|
+
method = self.METHODS.get(code_detail, f"0.{code_detail:02d}")
|
|
373
|
+
return method, True
|
|
374
|
+
|
|
375
|
+
# Response code
|
|
376
|
+
code_str = f"{code_class}.{code_detail:02d}"
|
|
377
|
+
|
|
378
|
+
# Add common response names
|
|
379
|
+
response_names = {
|
|
380
|
+
0x41: "2.01 Created",
|
|
381
|
+
0x42: "2.02 Deleted",
|
|
382
|
+
0x43: "2.03 Valid",
|
|
383
|
+
0x44: "2.04 Changed",
|
|
384
|
+
0x45: "2.05 Content",
|
|
385
|
+
0x5F: "2.31 Continue",
|
|
386
|
+
0x80: "4.00 Bad Request",
|
|
387
|
+
0x81: "4.01 Unauthorized",
|
|
388
|
+
0x82: "4.02 Bad Option",
|
|
389
|
+
0x83: "4.03 Forbidden",
|
|
390
|
+
0x84: "4.04 Not Found",
|
|
391
|
+
0x85: "4.05 Method Not Allowed",
|
|
392
|
+
0x86: "4.06 Not Acceptable",
|
|
393
|
+
0x8C: "4.12 Precondition Failed",
|
|
394
|
+
0x8D: "4.13 Request Entity Too Large",
|
|
395
|
+
0x8F: "4.15 Unsupported Content-Format",
|
|
396
|
+
0xA0: "5.00 Internal Server Error",
|
|
397
|
+
0xA1: "5.01 Not Implemented",
|
|
398
|
+
0xA2: "5.02 Bad Gateway",
|
|
399
|
+
0xA3: "5.03 Service Unavailable",
|
|
400
|
+
0xA4: "5.04 Gateway Timeout",
|
|
401
|
+
0xA5: "5.05 Proxying Not Supported",
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return response_names.get(code, code_str), False
|
|
405
|
+
|
|
406
|
+
def _parse_options(self, data: bytes, start_offset: int) -> dict[str, list[Any]]:
|
|
407
|
+
"""Parse CoAP options using delta encoding.
|
|
408
|
+
|
|
409
|
+
Options are encoded with delta encoding where each option number
|
|
410
|
+
is the sum of all previous deltas. Handles extended delta/length
|
|
411
|
+
encoding for values >= 13.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
data: Complete message data.
|
|
415
|
+
start_offset: Offset where options start.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Dictionary mapping option names to lists of decoded values.
|
|
419
|
+
|
|
420
|
+
Raises:
|
|
421
|
+
ValueError: If option encoding is invalid.
|
|
422
|
+
|
|
423
|
+
Example:
|
|
424
|
+
>>> analyzer = CoAPAnalyzer()
|
|
425
|
+
>>> # Parse message with Uri-Path option
|
|
426
|
+
>>> options = analyzer._parse_options(data, 5)
|
|
427
|
+
>>> options.get("Uri-Path", [])
|
|
428
|
+
['temperature', 'sensor1']
|
|
429
|
+
"""
|
|
430
|
+
options: dict[str, list[Any]] = {}
|
|
431
|
+
offset = start_offset
|
|
432
|
+
current_option_num = 0
|
|
433
|
+
|
|
434
|
+
while offset < len(data):
|
|
435
|
+
# Check for payload marker
|
|
436
|
+
if data[offset] == 0xFF:
|
|
437
|
+
break
|
|
438
|
+
|
|
439
|
+
if offset >= len(data):
|
|
440
|
+
break
|
|
441
|
+
|
|
442
|
+
option_byte = data[offset]
|
|
443
|
+
delta_base = (option_byte >> 4) & 0x0F
|
|
444
|
+
length_base = option_byte & 0x0F
|
|
445
|
+
offset += 1
|
|
446
|
+
|
|
447
|
+
# Check for payload marker in delta
|
|
448
|
+
if delta_base == 15:
|
|
449
|
+
offset -= 1 # Back up to marker
|
|
450
|
+
break
|
|
451
|
+
|
|
452
|
+
# Parse extended delta
|
|
453
|
+
try:
|
|
454
|
+
delta, delta_bytes = self.option_parser.parse_extended_value(
|
|
455
|
+
delta_base, data, offset
|
|
456
|
+
)
|
|
457
|
+
offset += delta_bytes
|
|
458
|
+
except ValueError as e:
|
|
459
|
+
raise ValueError(f"Failed to parse option delta: {e}") from e
|
|
460
|
+
|
|
461
|
+
# Parse extended length
|
|
462
|
+
try:
|
|
463
|
+
length, length_bytes = self.option_parser.parse_extended_value(
|
|
464
|
+
length_base, data, offset
|
|
465
|
+
)
|
|
466
|
+
offset += length_bytes
|
|
467
|
+
except ValueError as e:
|
|
468
|
+
raise ValueError(f"Failed to parse option length: {e}") from e
|
|
469
|
+
|
|
470
|
+
# Calculate actual option number
|
|
471
|
+
current_option_num += delta
|
|
472
|
+
|
|
473
|
+
# Extract option value
|
|
474
|
+
if len(data) < offset + length:
|
|
475
|
+
raise ValueError(
|
|
476
|
+
f"Insufficient data for option value: need {length} bytes at offset {offset}"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
option_value = data[offset : offset + length]
|
|
480
|
+
offset += length
|
|
481
|
+
|
|
482
|
+
# Decode option value
|
|
483
|
+
decoded_value = self.option_parser.decode_value(current_option_num, option_value)
|
|
484
|
+
|
|
485
|
+
# Store option
|
|
486
|
+
option_name = OPTIONS.get(current_option_num, f"Option-{current_option_num}")
|
|
487
|
+
|
|
488
|
+
if option_name not in options:
|
|
489
|
+
options[option_name] = []
|
|
490
|
+
|
|
491
|
+
options[option_name].append(decoded_value)
|
|
492
|
+
|
|
493
|
+
return options
|
|
494
|
+
|
|
495
|
+
def _reconstruct_uri(self, options: dict[str, list[Any]]) -> str | None:
|
|
496
|
+
"""Reconstruct URI from Uri-* options.
|
|
497
|
+
|
|
498
|
+
Combines Uri-Host, Uri-Port, Uri-Path, and Uri-Query options
|
|
499
|
+
into a complete URI string.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
options: Parsed options dictionary.
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
Reconstructed URI string, or None if no Uri options present.
|
|
506
|
+
|
|
507
|
+
Example:
|
|
508
|
+
>>> analyzer = CoAPAnalyzer()
|
|
509
|
+
>>> options = {
|
|
510
|
+
... "Uri-Host": ["example.com"],
|
|
511
|
+
... "Uri-Path": ["sensors", "temperature"],
|
|
512
|
+
... "Uri-Query": ["format=json"],
|
|
513
|
+
... }
|
|
514
|
+
>>> analyzer._reconstruct_uri(options)
|
|
515
|
+
'coap://example.com/sensors/temperature?format=json'
|
|
516
|
+
"""
|
|
517
|
+
if not any(key.startswith("Uri-") for key in options):
|
|
518
|
+
return None
|
|
519
|
+
|
|
520
|
+
# Build URI components
|
|
521
|
+
host = options.get("Uri-Host", [None])[0]
|
|
522
|
+
port = options.get("Uri-Port", [5683])[0] # Default CoAP port
|
|
523
|
+
path_segments = options.get("Uri-Path", [])
|
|
524
|
+
query_params = options.get("Uri-Query", [])
|
|
525
|
+
|
|
526
|
+
# Construct URI
|
|
527
|
+
uri_parts = []
|
|
528
|
+
|
|
529
|
+
if host:
|
|
530
|
+
scheme = "coap"
|
|
531
|
+
if isinstance(port, int) and port != 5683:
|
|
532
|
+
uri_parts.append(f"{scheme}://{host}:{port}")
|
|
533
|
+
else:
|
|
534
|
+
uri_parts.append(f"{scheme}://{host}")
|
|
535
|
+
else:
|
|
536
|
+
uri_parts.append("coap://")
|
|
537
|
+
|
|
538
|
+
# Add path
|
|
539
|
+
if path_segments:
|
|
540
|
+
path = "/" + "/".join(str(seg) for seg in path_segments)
|
|
541
|
+
uri_parts.append(path)
|
|
542
|
+
|
|
543
|
+
# Add query string
|
|
544
|
+
if query_params:
|
|
545
|
+
query = "&".join(str(param) for param in query_params)
|
|
546
|
+
uri_parts.append(f"?{query}")
|
|
547
|
+
|
|
548
|
+
return "".join(uri_parts)
|
|
549
|
+
|
|
550
|
+
def match_request_response(self) -> None:
|
|
551
|
+
"""Match requests with responses by token and message ID.
|
|
552
|
+
|
|
553
|
+
Creates CoAPExchange objects linking requests with their responses.
|
|
554
|
+
Supports observe relationships where multiple responses map to one request.
|
|
555
|
+
|
|
556
|
+
Example:
|
|
557
|
+
>>> analyzer = CoAPAnalyzer()
|
|
558
|
+
>>> # Parse messages...
|
|
559
|
+
>>> analyzer.match_request_response()
|
|
560
|
+
>>> len(analyzer.exchanges)
|
|
561
|
+
5
|
|
562
|
+
"""
|
|
563
|
+
# Clear existing exchanges
|
|
564
|
+
self.exchanges = {}
|
|
565
|
+
|
|
566
|
+
for message in self.messages:
|
|
567
|
+
if message.is_request:
|
|
568
|
+
# Create new exchange for request
|
|
569
|
+
observe = "Observe" in message.options
|
|
570
|
+
exchange = CoAPExchange(request=message, observe=observe)
|
|
571
|
+
self.exchanges[message.token] = exchange
|
|
572
|
+
else:
|
|
573
|
+
# Match response to request by token
|
|
574
|
+
if message.token in self.exchanges:
|
|
575
|
+
exchange = self.exchanges[message.token]
|
|
576
|
+
exchange.responses.append(message)
|
|
577
|
+
|
|
578
|
+
# Mark complete if not observe or if ACK/RST
|
|
579
|
+
if not exchange.observe or message.msg_type in ("ACK", "RST"):
|
|
580
|
+
exchange.complete = True
|
|
581
|
+
|
|
582
|
+
def export_exchanges(self, output_path: Path) -> None:
|
|
583
|
+
"""Export request-response exchanges as JSON.
|
|
584
|
+
|
|
585
|
+
Exports all matched exchanges including request, responses, timing,
|
|
586
|
+
and decoded options.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
output_path: Path to output JSON file.
|
|
590
|
+
|
|
591
|
+
Example:
|
|
592
|
+
>>> analyzer = CoAPAnalyzer()
|
|
593
|
+
>>> # Parse and match messages...
|
|
594
|
+
>>> analyzer.export_exchanges(Path("coap_exchanges.json"))
|
|
595
|
+
"""
|
|
596
|
+
export_data: dict[str, Any] = {
|
|
597
|
+
"summary": {
|
|
598
|
+
"total_messages": len(self.messages),
|
|
599
|
+
"total_exchanges": len(self.exchanges),
|
|
600
|
+
"complete_exchanges": sum(1 for ex in self.exchanges.values() if ex.complete),
|
|
601
|
+
},
|
|
602
|
+
"exchanges": [],
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
for token, exchange in self.exchanges.items():
|
|
606
|
+
request_data = self._message_to_dict(exchange.request)
|
|
607
|
+
|
|
608
|
+
responses_data = [self._message_to_dict(resp) for resp in exchange.responses]
|
|
609
|
+
|
|
610
|
+
exchange_entry = {
|
|
611
|
+
"token": token.hex() if token else "",
|
|
612
|
+
"observe": exchange.observe,
|
|
613
|
+
"complete": exchange.complete,
|
|
614
|
+
"request": request_data,
|
|
615
|
+
"responses": responses_data,
|
|
616
|
+
"response_count": len(exchange.responses),
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export_data["exchanges"].append(exchange_entry)
|
|
620
|
+
|
|
621
|
+
# Write JSON
|
|
622
|
+
with output_path.open("w") as f:
|
|
623
|
+
json.dump(export_data, f, indent=2)
|
|
624
|
+
|
|
625
|
+
def _message_to_dict(self, message: CoAPMessage) -> dict[str, Any]:
|
|
626
|
+
"""Convert CoAPMessage to dictionary for JSON export.
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
message: CoAP message to convert.
|
|
630
|
+
|
|
631
|
+
Returns:
|
|
632
|
+
Dictionary representation of message.
|
|
633
|
+
"""
|
|
634
|
+
# Format options for readability
|
|
635
|
+
formatted_options: dict[str, list[Any]] = {}
|
|
636
|
+
for name, values in message.options.items():
|
|
637
|
+
formatted_values: list[Any] = []
|
|
638
|
+
for value in values:
|
|
639
|
+
if isinstance(value, bytes):
|
|
640
|
+
formatted_values.append(value.hex())
|
|
641
|
+
elif isinstance(value, int) and name == "Content-Format":
|
|
642
|
+
# Add content format name
|
|
643
|
+
format_name = CONTENT_FORMATS.get(value, "unknown")
|
|
644
|
+
formatted_values.append({"code": value, "format": format_name})
|
|
645
|
+
elif isinstance(value, int) and name in ("Block1", "Block2"):
|
|
646
|
+
# Decode block option
|
|
647
|
+
block_info = format_block_option(value)
|
|
648
|
+
formatted_values.append(block_info)
|
|
649
|
+
else:
|
|
650
|
+
formatted_values.append(value)
|
|
651
|
+
formatted_options[name] = formatted_values
|
|
652
|
+
|
|
653
|
+
return {
|
|
654
|
+
"timestamp": message.timestamp,
|
|
655
|
+
"type": message.msg_type,
|
|
656
|
+
"code": message.code,
|
|
657
|
+
"message_id": f"0x{message.message_id:04X}",
|
|
658
|
+
"token": message.token.hex() if message.token else "",
|
|
659
|
+
"uri": message.uri,
|
|
660
|
+
"options": formatted_options,
|
|
661
|
+
"payload_length": len(message.payload),
|
|
662
|
+
"payload": message.payload.hex()
|
|
663
|
+
if len(message.payload) <= 64
|
|
664
|
+
else f"{message.payload[:64].hex()}... ({len(message.payload)} bytes)",
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
__all__ = ["CoAPAnalyzer", "CoAPExchange", "CoAPMessage"]
|