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,153 @@
|
|
|
1
|
+
"""Zigbee security and encryption support.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for handling Zigbee security frames
|
|
4
|
+
including network key management and frame decryption support.
|
|
5
|
+
|
|
6
|
+
Note: This module provides structure parsing only. Actual AES-CCM
|
|
7
|
+
decryption requires cryptography libraries and valid network keys.
|
|
8
|
+
|
|
9
|
+
References:
|
|
10
|
+
Zigbee Specification Section 4.5 (Security)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_security_header(data: bytes) -> dict[str, Any]:
|
|
19
|
+
"""Parse Zigbee security header from NWK auxiliary header.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
data: Security header bytes.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Parsed security header fields.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> header = parse_security_header(security_data)
|
|
29
|
+
>>> print(header['security_level'])
|
|
30
|
+
5
|
|
31
|
+
"""
|
|
32
|
+
if len(data) < 5:
|
|
33
|
+
return {"error": "Insufficient data for security header"}
|
|
34
|
+
|
|
35
|
+
security_control = data[0]
|
|
36
|
+
frame_counter = int.from_bytes(data[1:5], "little")
|
|
37
|
+
|
|
38
|
+
security_level = security_control & 0x07
|
|
39
|
+
key_identifier = (security_control >> 3) & 0x03
|
|
40
|
+
extended_nonce = bool(security_control & 0x20)
|
|
41
|
+
|
|
42
|
+
result: dict[str, Any] = {
|
|
43
|
+
"security_control": security_control,
|
|
44
|
+
"security_level": security_level,
|
|
45
|
+
"key_identifier": key_identifier,
|
|
46
|
+
"extended_nonce": extended_nonce,
|
|
47
|
+
"frame_counter": frame_counter,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
offset = 5
|
|
51
|
+
|
|
52
|
+
# Extended nonce includes source address
|
|
53
|
+
if extended_nonce:
|
|
54
|
+
if len(data) < offset + 8:
|
|
55
|
+
result["error"] = "Insufficient data for extended nonce"
|
|
56
|
+
return result
|
|
57
|
+
source_address = int.from_bytes(data[offset : offset + 8], "little")
|
|
58
|
+
result["source_address"] = source_address
|
|
59
|
+
offset += 8
|
|
60
|
+
|
|
61
|
+
# Key sequence number (if key identifier indicates it)
|
|
62
|
+
if key_identifier == 0x01: # Network key
|
|
63
|
+
if len(data) < offset + 1:
|
|
64
|
+
result["error"] = "Insufficient data for key sequence"
|
|
65
|
+
return result
|
|
66
|
+
result["key_sequence_number"] = data[offset]
|
|
67
|
+
offset += 1
|
|
68
|
+
|
|
69
|
+
result["header_length"] = offset
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_frame_encrypted(nwk_frame_control: int) -> bool:
|
|
74
|
+
"""Check if NWK frame is encrypted.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
nwk_frame_control: NWK frame control field (2 bytes as int).
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
True if frame is encrypted, False otherwise.
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
>>> frame_control = 0x0208 # Security enabled
|
|
84
|
+
>>> is_frame_encrypted(frame_control)
|
|
85
|
+
True
|
|
86
|
+
"""
|
|
87
|
+
# Security bit is bit 1 of the high byte (bit 9 overall)
|
|
88
|
+
return bool((nwk_frame_control >> 9) & 0x01)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_security_level_name(level: int) -> str:
|
|
92
|
+
"""Get human-readable security level name.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
level: Security level (0-7).
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Security level name.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
>>> get_security_level_name(5)
|
|
102
|
+
'ENC-MIC-32'
|
|
103
|
+
"""
|
|
104
|
+
levels = {
|
|
105
|
+
0x00: "None",
|
|
106
|
+
0x01: "MIC-32",
|
|
107
|
+
0x02: "MIC-64",
|
|
108
|
+
0x03: "MIC-128",
|
|
109
|
+
0x04: "ENC",
|
|
110
|
+
0x05: "ENC-MIC-32",
|
|
111
|
+
0x06: "ENC-MIC-64",
|
|
112
|
+
0x07: "ENC-MIC-128",
|
|
113
|
+
}
|
|
114
|
+
return levels.get(level, f"Unknown ({level})")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def decrypt_frame(
|
|
118
|
+
encrypted_payload: bytes,
|
|
119
|
+
network_key: bytes,
|
|
120
|
+
nonce: bytes,
|
|
121
|
+
security_level: int,
|
|
122
|
+
) -> bytes | None:
|
|
123
|
+
"""Decrypt Zigbee encrypted frame.
|
|
124
|
+
|
|
125
|
+
Note: This is a placeholder implementation. Actual decryption
|
|
126
|
+
requires AES-CCM implementation with proper nonce construction.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
encrypted_payload: Encrypted payload with MIC.
|
|
130
|
+
network_key: 128-bit AES network key.
|
|
131
|
+
nonce: 13-byte nonce.
|
|
132
|
+
security_level: Security level (determines MIC size).
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Decrypted payload or None if decryption not available.
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
>>> # Requires cryptography library
|
|
139
|
+
>>> decrypted = decrypt_frame(payload, key, nonce, 5)
|
|
140
|
+
"""
|
|
141
|
+
# Placeholder - real implementation would use AES-CCM
|
|
142
|
+
# from cryptography.hazmat.primitives.ciphers.aead import AESCCM
|
|
143
|
+
# cipher = AESCCM(network_key)
|
|
144
|
+
# return cipher.decrypt(nonce, encrypted_payload, None)
|
|
145
|
+
return None # Not implemented without cryptography dependencies
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
__all__ = [
|
|
149
|
+
"decrypt_frame",
|
|
150
|
+
"get_security_level_name",
|
|
151
|
+
"is_frame_encrypted",
|
|
152
|
+
"parse_security_header",
|
|
153
|
+
]
|
oscura/iot/zigbee/zcl.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""Zigbee Cluster Library (ZCL) definitions and parsers.
|
|
2
|
+
|
|
3
|
+
This module contains standard ZCL cluster definitions and frame parsers
|
|
4
|
+
for common clusters including On/Off, Level Control, Temperature, etc.
|
|
5
|
+
|
|
6
|
+
References:
|
|
7
|
+
Zigbee Cluster Library Specification (CSA-IOT)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
# Standard ZCL cluster IDs (subset of most common clusters)
|
|
15
|
+
ZCL_CLUSTERS: dict[int, str] = {
|
|
16
|
+
0x0000: "Basic",
|
|
17
|
+
0x0001: "Power Configuration",
|
|
18
|
+
0x0003: "Identify",
|
|
19
|
+
0x0004: "Groups",
|
|
20
|
+
0x0005: "Scenes",
|
|
21
|
+
0x0006: "On/Off",
|
|
22
|
+
0x0008: "Level Control",
|
|
23
|
+
0x0009: "Alarms",
|
|
24
|
+
0x000A: "Time",
|
|
25
|
+
0x0020: "Poll Control",
|
|
26
|
+
0x0201: "Thermostat",
|
|
27
|
+
0x0300: "Color Control",
|
|
28
|
+
0x0400: "Illuminance Measurement",
|
|
29
|
+
0x0402: "Temperature Measurement",
|
|
30
|
+
0x0403: "Pressure Measurement",
|
|
31
|
+
0x0404: "Flow Measurement",
|
|
32
|
+
0x0405: "Relative Humidity Measurement",
|
|
33
|
+
0x0406: "Occupancy Sensing",
|
|
34
|
+
0x0500: "IAS Zone",
|
|
35
|
+
0x0702: "Metering",
|
|
36
|
+
0x0B04: "Electrical Measurement",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# ZCL frame control field bits
|
|
40
|
+
ZCL_FRAME_TYPE_MASK = 0x03
|
|
41
|
+
ZCL_FRAME_TYPE_GLOBAL = 0x00
|
|
42
|
+
ZCL_FRAME_TYPE_CLUSTER = 0x01
|
|
43
|
+
|
|
44
|
+
ZCL_MANUFACTURER_SPECIFIC = 0x04
|
|
45
|
+
ZCL_DIRECTION_SERVER_TO_CLIENT = 0x08
|
|
46
|
+
ZCL_DISABLE_DEFAULT_RESPONSE = 0x10
|
|
47
|
+
|
|
48
|
+
# Global ZCL commands
|
|
49
|
+
ZCL_GLOBAL_COMMANDS = {
|
|
50
|
+
0x00: "Read Attributes",
|
|
51
|
+
0x01: "Read Attributes Response",
|
|
52
|
+
0x02: "Write Attributes",
|
|
53
|
+
0x03: "Write Attributes Undivided",
|
|
54
|
+
0x04: "Write Attributes Response",
|
|
55
|
+
0x05: "Write Attributes No Response",
|
|
56
|
+
0x06: "Configure Reporting",
|
|
57
|
+
0x07: "Configure Reporting Response",
|
|
58
|
+
0x08: "Read Reporting Configuration",
|
|
59
|
+
0x09: "Read Reporting Configuration Response",
|
|
60
|
+
0x0A: "Report Attributes",
|
|
61
|
+
0x0B: "Default Response",
|
|
62
|
+
0x0C: "Discover Attributes",
|
|
63
|
+
0x0D: "Discover Attributes Response",
|
|
64
|
+
0x0E: "Read Attributes Structured",
|
|
65
|
+
0x0F: "Write Attributes Structured",
|
|
66
|
+
0x10: "Write Attributes Structured Response",
|
|
67
|
+
0x11: "Discover Commands Received",
|
|
68
|
+
0x12: "Discover Commands Received Response",
|
|
69
|
+
0x13: "Discover Commands Generated",
|
|
70
|
+
0x14: "Discover Commands Generated Response",
|
|
71
|
+
0x15: "Discover Attributes Extended",
|
|
72
|
+
0x16: "Discover Attributes Extended Response",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# On/Off cluster (0x0006) commands
|
|
76
|
+
ZCL_ONOFF_COMMANDS = {
|
|
77
|
+
0x00: "Off",
|
|
78
|
+
0x01: "On",
|
|
79
|
+
0x02: "Toggle",
|
|
80
|
+
0x40: "Off With Effect",
|
|
81
|
+
0x41: "On With Recall Global Scene",
|
|
82
|
+
0x42: "On With Timed Off",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Level Control cluster (0x0008) commands
|
|
86
|
+
ZCL_LEVEL_CONTROL_COMMANDS = {
|
|
87
|
+
0x00: "Move to Level",
|
|
88
|
+
0x01: "Move",
|
|
89
|
+
0x02: "Step",
|
|
90
|
+
0x03: "Stop",
|
|
91
|
+
0x04: "Move to Level (with On/Off)",
|
|
92
|
+
0x05: "Move (with On/Off)",
|
|
93
|
+
0x06: "Step (with On/Off)",
|
|
94
|
+
0x07: "Stop (with On/Off)",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Color Control cluster (0x0300) commands
|
|
98
|
+
ZCL_COLOR_CONTROL_COMMANDS = {
|
|
99
|
+
0x00: "Move to Hue",
|
|
100
|
+
0x01: "Move Hue",
|
|
101
|
+
0x02: "Step Hue",
|
|
102
|
+
0x03: "Move to Saturation",
|
|
103
|
+
0x04: "Move Saturation",
|
|
104
|
+
0x05: "Step Saturation",
|
|
105
|
+
0x06: "Move to Hue and Saturation",
|
|
106
|
+
0x07: "Move to Color",
|
|
107
|
+
0x08: "Move Color",
|
|
108
|
+
0x09: "Step Color",
|
|
109
|
+
0x0A: "Move to Color Temperature",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def parse_zcl_frame(cluster_id: int, data: bytes) -> dict[str, Any]:
|
|
114
|
+
"""Parse ZCL frame for specific cluster.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
cluster_id: ZCL cluster ID (e.g., 0x0006 for On/Off).
|
|
118
|
+
data: ZCL frame payload.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Parsed ZCL frame with cluster-specific details.
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
>>> data = bytes([0x01, 0x00, 0x01]) # On/Off cluster, On command
|
|
125
|
+
>>> result = parse_zcl_frame(0x0006, data)
|
|
126
|
+
>>> print(result['command_name'])
|
|
127
|
+
On
|
|
128
|
+
"""
|
|
129
|
+
if len(data) < 3:
|
|
130
|
+
return {
|
|
131
|
+
"error": "Insufficient ZCL data",
|
|
132
|
+
"cluster_id": cluster_id,
|
|
133
|
+
"cluster_name": ZCL_CLUSTERS.get(cluster_id, "Unknown"),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
frame_control = data[0]
|
|
137
|
+
transaction_seq = data[1]
|
|
138
|
+
command_id = data[2]
|
|
139
|
+
|
|
140
|
+
frame_type = frame_control & ZCL_FRAME_TYPE_MASK
|
|
141
|
+
manufacturer_specific = bool(frame_control & ZCL_MANUFACTURER_SPECIFIC)
|
|
142
|
+
direction = (
|
|
143
|
+
"server_to_client"
|
|
144
|
+
if (frame_control & ZCL_DIRECTION_SERVER_TO_CLIENT)
|
|
145
|
+
else "client_to_server"
|
|
146
|
+
)
|
|
147
|
+
disable_default_response = bool(frame_control & ZCL_DISABLE_DEFAULT_RESPONSE)
|
|
148
|
+
|
|
149
|
+
result: dict[str, Any] = {
|
|
150
|
+
"cluster_id": cluster_id,
|
|
151
|
+
"cluster_name": ZCL_CLUSTERS.get(cluster_id, f"Unknown (0x{cluster_id:04X})"),
|
|
152
|
+
"frame_control": frame_control,
|
|
153
|
+
"frame_type": "global" if frame_type == ZCL_FRAME_TYPE_GLOBAL else "cluster_specific",
|
|
154
|
+
"manufacturer_specific": manufacturer_specific,
|
|
155
|
+
"direction": direction,
|
|
156
|
+
"disable_default_response": disable_default_response,
|
|
157
|
+
"transaction_sequence": transaction_seq,
|
|
158
|
+
"command_id": command_id,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Parse manufacturer code if present
|
|
162
|
+
offset = 3
|
|
163
|
+
if manufacturer_specific:
|
|
164
|
+
if len(data) >= 5:
|
|
165
|
+
mfr_code = int.from_bytes(data[3:5], "little")
|
|
166
|
+
result["manufacturer_code"] = mfr_code
|
|
167
|
+
offset = 5
|
|
168
|
+
else:
|
|
169
|
+
result["error"] = "Insufficient data for manufacturer code"
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
payload = data[offset:]
|
|
173
|
+
result["payload"] = payload
|
|
174
|
+
|
|
175
|
+
# Parse cluster-specific commands
|
|
176
|
+
if frame_type == ZCL_FRAME_TYPE_GLOBAL:
|
|
177
|
+
result["command_name"] = ZCL_GLOBAL_COMMANDS.get(
|
|
178
|
+
command_id, f"Unknown Global (0x{command_id:02X})"
|
|
179
|
+
)
|
|
180
|
+
if command_id == 0x00: # Read Attributes
|
|
181
|
+
result["details"] = _parse_read_attributes(payload)
|
|
182
|
+
elif command_id == 0x01: # Read Attributes Response
|
|
183
|
+
result["details"] = _parse_read_attributes_response(payload)
|
|
184
|
+
elif command_id == 0x0A: # Report Attributes
|
|
185
|
+
result["details"] = _parse_report_attributes(payload)
|
|
186
|
+
else:
|
|
187
|
+
# Cluster-specific command
|
|
188
|
+
if cluster_id == 0x0006: # On/Off
|
|
189
|
+
result["command_name"] = ZCL_ONOFF_COMMANDS.get(
|
|
190
|
+
command_id, f"Unknown (0x{command_id:02X})"
|
|
191
|
+
)
|
|
192
|
+
if command_id in [0x00, 0x01, 0x02]: # Off, On, Toggle
|
|
193
|
+
result["details"] = {"simple_command": True}
|
|
194
|
+
elif cluster_id == 0x0008: # Level Control
|
|
195
|
+
result["command_name"] = ZCL_LEVEL_CONTROL_COMMANDS.get(
|
|
196
|
+
command_id, f"Unknown (0x{command_id:02X})"
|
|
197
|
+
)
|
|
198
|
+
if command_id == 0x00 and len(payload) >= 2: # Move to Level
|
|
199
|
+
result["details"] = {
|
|
200
|
+
"level": payload[0],
|
|
201
|
+
"transition_time": int.from_bytes(payload[1:3], "little")
|
|
202
|
+
if len(payload) >= 3
|
|
203
|
+
else None,
|
|
204
|
+
}
|
|
205
|
+
elif cluster_id == 0x0300: # Color Control
|
|
206
|
+
result["command_name"] = ZCL_COLOR_CONTROL_COMMANDS.get(
|
|
207
|
+
command_id, f"Unknown (0x{command_id:02X})"
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
result["command_name"] = f"Cluster Command 0x{command_id:02X}"
|
|
211
|
+
|
|
212
|
+
return result
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _parse_read_attributes(payload: bytes) -> dict[str, Any]:
|
|
216
|
+
"""Parse Read Attributes command payload.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
payload: ZCL payload after command ID.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Parsed attribute IDs.
|
|
223
|
+
"""
|
|
224
|
+
if len(payload) < 2:
|
|
225
|
+
return {"error": "Insufficient data"}
|
|
226
|
+
|
|
227
|
+
attribute_ids = []
|
|
228
|
+
offset = 0
|
|
229
|
+
while offset + 2 <= len(payload):
|
|
230
|
+
attr_id = int.from_bytes(payload[offset : offset + 2], "little")
|
|
231
|
+
attribute_ids.append(attr_id)
|
|
232
|
+
offset += 2
|
|
233
|
+
|
|
234
|
+
return {"attribute_ids": attribute_ids}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _parse_read_attributes_response(payload: bytes) -> dict[str, Any]:
|
|
238
|
+
"""Parse Read Attributes Response payload.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
payload: ZCL payload after command ID.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Parsed attribute records.
|
|
245
|
+
"""
|
|
246
|
+
if len(payload) < 3:
|
|
247
|
+
return {"error": "Insufficient data"}
|
|
248
|
+
|
|
249
|
+
attributes = []
|
|
250
|
+
offset = 0
|
|
251
|
+
while offset + 3 <= len(payload):
|
|
252
|
+
attr_id = int.from_bytes(payload[offset : offset + 2], "little")
|
|
253
|
+
status = payload[offset + 2]
|
|
254
|
+
offset += 3
|
|
255
|
+
|
|
256
|
+
attr_record: dict[str, Any] = {
|
|
257
|
+
"attribute_id": attr_id,
|
|
258
|
+
"status": status,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if status == 0x00: # Success
|
|
262
|
+
if offset >= len(payload):
|
|
263
|
+
break
|
|
264
|
+
data_type = payload[offset]
|
|
265
|
+
offset += 1
|
|
266
|
+
|
|
267
|
+
# Parse value based on data type (simplified)
|
|
268
|
+
if data_type == 0x10: # Boolean
|
|
269
|
+
if offset < len(payload):
|
|
270
|
+
attr_record["value"] = bool(payload[offset])
|
|
271
|
+
offset += 1
|
|
272
|
+
elif data_type == 0x20: # Uint8
|
|
273
|
+
if offset < len(payload):
|
|
274
|
+
attr_record["value"] = payload[offset]
|
|
275
|
+
offset += 1
|
|
276
|
+
elif data_type == 0x21: # Uint16
|
|
277
|
+
if offset + 2 <= len(payload):
|
|
278
|
+
attr_record["value"] = int.from_bytes(payload[offset : offset + 2], "little")
|
|
279
|
+
offset += 2
|
|
280
|
+
elif data_type == 0x29: # Int16
|
|
281
|
+
if offset + 2 <= len(payload):
|
|
282
|
+
attr_record["value"] = int.from_bytes(
|
|
283
|
+
payload[offset : offset + 2], "little", signed=True
|
|
284
|
+
)
|
|
285
|
+
offset += 2
|
|
286
|
+
else:
|
|
287
|
+
attr_record["data_type"] = data_type
|
|
288
|
+
# Skip unknown data type
|
|
289
|
+
break
|
|
290
|
+
|
|
291
|
+
attributes.append(attr_record)
|
|
292
|
+
|
|
293
|
+
return {"attributes": attributes}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _parse_report_attributes(payload: bytes) -> dict[str, Any]:
|
|
297
|
+
"""Parse Report Attributes payload.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
payload: ZCL payload after command ID.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Parsed attribute reports.
|
|
304
|
+
"""
|
|
305
|
+
# Similar to Read Attributes Response but without status field
|
|
306
|
+
if len(payload) < 3:
|
|
307
|
+
return {"error": "Insufficient data"}
|
|
308
|
+
|
|
309
|
+
attributes = []
|
|
310
|
+
offset = 0
|
|
311
|
+
while offset + 3 <= len(payload):
|
|
312
|
+
attr_id = int.from_bytes(payload[offset : offset + 2], "little")
|
|
313
|
+
data_type = payload[offset + 2]
|
|
314
|
+
offset += 3
|
|
315
|
+
|
|
316
|
+
attr_record: dict[str, Any] = {
|
|
317
|
+
"attribute_id": attr_id,
|
|
318
|
+
"data_type": data_type,
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# Parse value based on data type (simplified)
|
|
322
|
+
if data_type == 0x10: # Boolean
|
|
323
|
+
if offset < len(payload):
|
|
324
|
+
attr_record["value"] = bool(payload[offset])
|
|
325
|
+
offset += 1
|
|
326
|
+
elif data_type == 0x20: # Uint8
|
|
327
|
+
if offset < len(payload):
|
|
328
|
+
attr_record["value"] = payload[offset]
|
|
329
|
+
offset += 1
|
|
330
|
+
elif data_type == 0x21: # Uint16
|
|
331
|
+
if offset + 2 <= len(payload):
|
|
332
|
+
attr_record["value"] = int.from_bytes(payload[offset : offset + 2], "little")
|
|
333
|
+
offset += 2
|
|
334
|
+
elif data_type == 0x29: # Int16
|
|
335
|
+
if offset + 2 <= len(payload):
|
|
336
|
+
attr_record["value"] = int.from_bytes(
|
|
337
|
+
payload[offset : offset + 2], "little", signed=True
|
|
338
|
+
)
|
|
339
|
+
offset += 2
|
|
340
|
+
else:
|
|
341
|
+
# Skip unknown data type
|
|
342
|
+
break
|
|
343
|
+
|
|
344
|
+
attributes.append(attr_record)
|
|
345
|
+
|
|
346
|
+
return {"attributes": attributes}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
__all__ = ["ZCL_CLUSTERS", "parse_zcl_frame"]
|
oscura/jupyter/display.py
CHANGED
|
@@ -15,9 +15,15 @@ Example:
|
|
|
15
15
|
from typing import Any
|
|
16
16
|
|
|
17
17
|
try:
|
|
18
|
-
from IPython.display import HTML, SVG
|
|
18
|
+
from IPython.display import HTML, SVG
|
|
19
|
+
from IPython.display import display as ipython_display
|
|
19
20
|
|
|
20
21
|
IPYTHON_AVAILABLE = True
|
|
22
|
+
|
|
23
|
+
def display(*args: Any, **kwargs: Any) -> None:
|
|
24
|
+
"""Wrapper for IPython display."""
|
|
25
|
+
ipython_display(*args, **kwargs) # type: ignore[no-untyped-call]
|
|
26
|
+
|
|
21
27
|
except ImportError:
|
|
22
28
|
IPYTHON_AVAILABLE = False
|
|
23
29
|
|
|
@@ -33,7 +39,7 @@ except ImportError:
|
|
|
33
39
|
def __init__(self, data: str) -> None:
|
|
34
40
|
self.data = data
|
|
35
41
|
|
|
36
|
-
def display(*args: Any, **kwargs: Any) -> None:
|
|
42
|
+
def display(*args: Any, **kwargs: Any) -> None:
|
|
37
43
|
"""Fallback display when IPython not available."""
|
|
38
44
|
for arg in args:
|
|
39
45
|
print(arg)
|
|
@@ -58,56 +64,130 @@ class TraceDisplay:
|
|
|
58
64
|
def _repr_html_(self) -> str:
|
|
59
65
|
"""Generate HTML representation for Jupyter."""
|
|
60
66
|
trace = self.trace
|
|
67
|
+
rows: list[tuple[str, str]] = []
|
|
68
|
+
|
|
69
|
+
# Collect trace information
|
|
70
|
+
self._add_basic_rows(trace, rows)
|
|
71
|
+
self._add_metadata_rows(trace, rows)
|
|
72
|
+
self._add_duration_row(trace, rows)
|
|
73
|
+
self._add_statistics_rows(trace, rows)
|
|
61
74
|
|
|
62
|
-
# Build
|
|
63
|
-
rows
|
|
75
|
+
# Build HTML table
|
|
76
|
+
return self._build_html_table(rows)
|
|
77
|
+
|
|
78
|
+
def _add_basic_rows(self, trace: Any, rows: list[tuple[str, str]]) -> None:
|
|
79
|
+
"""Add basic trace information rows.
|
|
64
80
|
|
|
81
|
+
Args:
|
|
82
|
+
trace: Trace object.
|
|
83
|
+
rows: List of (label, value) tuples.
|
|
84
|
+
"""
|
|
65
85
|
if hasattr(trace, "data"):
|
|
66
86
|
rows.append(("Samples", f"{len(trace.data):,}"))
|
|
67
87
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if hasattr(meta, "sample_rate") and meta.sample_rate:
|
|
71
|
-
rate = meta.sample_rate
|
|
72
|
-
if rate >= 1e9:
|
|
73
|
-
rate_str = f"{rate / 1e9:.3f} GSa/s"
|
|
74
|
-
elif rate >= 1e6:
|
|
75
|
-
rate_str = f"{rate / 1e6:.3f} MSa/s"
|
|
76
|
-
else:
|
|
77
|
-
rate_str = f"{rate / 1e3:.3f} kSa/s"
|
|
78
|
-
rows.append(("Sample Rate", rate_str))
|
|
79
|
-
|
|
80
|
-
if hasattr(meta, "channel_name") and meta.channel_name:
|
|
81
|
-
rows.append(("Channel", meta.channel_name))
|
|
82
|
-
|
|
83
|
-
if hasattr(meta, "source_file") and meta.source_file:
|
|
84
|
-
rows.append(("Source", meta.source_file))
|
|
85
|
-
|
|
86
|
-
# Calculate duration if possible
|
|
87
|
-
if hasattr(trace, "data") and hasattr(trace, "metadata"):
|
|
88
|
-
if hasattr(trace.metadata, "sample_rate") and trace.metadata.sample_rate:
|
|
89
|
-
duration = len(trace.data) / trace.metadata.sample_rate
|
|
90
|
-
if duration >= 1:
|
|
91
|
-
dur_str = f"{duration:.3f} s"
|
|
92
|
-
elif duration >= 1e-3:
|
|
93
|
-
dur_str = f"{duration * 1e3:.3f} ms"
|
|
94
|
-
elif duration >= 1e-6:
|
|
95
|
-
dur_str = f"{duration * 1e6:.3f} us"
|
|
96
|
-
else:
|
|
97
|
-
dur_str = f"{duration * 1e9:.3f} ns"
|
|
98
|
-
rows.append(("Duration", dur_str))
|
|
99
|
-
|
|
100
|
-
# Data statistics
|
|
101
|
-
if hasattr(trace, "data"):
|
|
102
|
-
import numpy as np
|
|
88
|
+
def _add_metadata_rows(self, trace: Any, rows: list[tuple[str, str]]) -> None:
|
|
89
|
+
"""Add metadata information rows.
|
|
103
90
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
rows
|
|
107
|
-
|
|
108
|
-
|
|
91
|
+
Args:
|
|
92
|
+
trace: Trace object.
|
|
93
|
+
rows: List of (label, value) tuples.
|
|
94
|
+
"""
|
|
95
|
+
if not hasattr(trace, "metadata"):
|
|
96
|
+
return
|
|
109
97
|
|
|
110
|
-
|
|
98
|
+
meta = trace.metadata
|
|
99
|
+
|
|
100
|
+
# Sample rate
|
|
101
|
+
if hasattr(meta, "sample_rate") and meta.sample_rate:
|
|
102
|
+
rate_str = self._format_sample_rate(meta.sample_rate)
|
|
103
|
+
rows.append(("Sample Rate", rate_str))
|
|
104
|
+
|
|
105
|
+
# Channel name
|
|
106
|
+
if hasattr(meta, "channel_name") and meta.channel_name:
|
|
107
|
+
rows.append(("Channel", meta.channel_name))
|
|
108
|
+
|
|
109
|
+
# Source file
|
|
110
|
+
if hasattr(meta, "source_file") and meta.source_file:
|
|
111
|
+
rows.append(("Source", meta.source_file))
|
|
112
|
+
|
|
113
|
+
def _format_sample_rate(self, rate: float) -> str:
|
|
114
|
+
"""Format sample rate with appropriate units.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
rate: Sample rate in Hz.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Formatted string with units.
|
|
121
|
+
"""
|
|
122
|
+
if rate >= 1e9:
|
|
123
|
+
return f"{rate / 1e9:.3f} GSa/s"
|
|
124
|
+
elif rate >= 1e6:
|
|
125
|
+
return f"{rate / 1e6:.3f} MSa/s"
|
|
126
|
+
else:
|
|
127
|
+
return f"{rate / 1e3:.3f} kSa/s"
|
|
128
|
+
|
|
129
|
+
def _add_duration_row(self, trace: Any, rows: list[tuple[str, str]]) -> None:
|
|
130
|
+
"""Add duration information row.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
trace: Trace object.
|
|
134
|
+
rows: List of (label, value) tuples.
|
|
135
|
+
"""
|
|
136
|
+
if not (hasattr(trace, "data") and hasattr(trace, "metadata")):
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
if not (hasattr(trace.metadata, "sample_rate") and trace.metadata.sample_rate):
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
duration = len(trace.data) / trace.metadata.sample_rate
|
|
143
|
+
dur_str = self._format_duration(duration)
|
|
144
|
+
rows.append(("Duration", dur_str))
|
|
145
|
+
|
|
146
|
+
def _format_duration(self, duration: float) -> str:
|
|
147
|
+
"""Format duration with appropriate units.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
duration: Duration in seconds.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Formatted string with units.
|
|
154
|
+
"""
|
|
155
|
+
if duration >= 1:
|
|
156
|
+
return f"{duration:.3f} s"
|
|
157
|
+
elif duration >= 1e-3:
|
|
158
|
+
return f"{duration * 1e3:.3f} ms"
|
|
159
|
+
elif duration >= 1e-6:
|
|
160
|
+
return f"{duration * 1e6:.3f} us"
|
|
161
|
+
else:
|
|
162
|
+
return f"{duration * 1e9:.3f} ns"
|
|
163
|
+
|
|
164
|
+
def _add_statistics_rows(self, trace: Any, rows: list[tuple[str, str]]) -> None:
|
|
165
|
+
"""Add data statistics rows.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
trace: Trace object.
|
|
169
|
+
rows: List of (label, value) tuples.
|
|
170
|
+
"""
|
|
171
|
+
if not hasattr(trace, "data"):
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
import numpy as np
|
|
175
|
+
|
|
176
|
+
data = trace.data
|
|
177
|
+
rows.append(("Min", f"{np.min(data):.4g}"))
|
|
178
|
+
rows.append(("Max", f"{np.max(data):.4g}"))
|
|
179
|
+
rows.append(("Mean", f"{np.mean(data):.4g}"))
|
|
180
|
+
rows.append(("Std Dev", f"{np.std(data):.4g}"))
|
|
181
|
+
|
|
182
|
+
def _build_html_table(self, rows: list[tuple[str, str]]) -> str:
|
|
183
|
+
"""Build HTML table from rows.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
rows: List of (label, value) tuples.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
HTML string.
|
|
190
|
+
"""
|
|
111
191
|
html = f"""
|
|
112
192
|
<div style="border: 1px solid #ccc; border-radius: 4px; padding: 10px; max-width: 400px;">
|
|
113
193
|
<h4 style="margin: 0 0 10px 0; color: #333;">{self.title}</h4>
|