oscura 0.0.1__py3-none-any.whl → 0.1.1__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 +813 -8
- oscura/__main__.py +392 -0
- oscura/analyzers/__init__.py +37 -0
- oscura/analyzers/digital/__init__.py +177 -0
- oscura/analyzers/digital/bus.py +691 -0
- oscura/analyzers/digital/clock.py +805 -0
- oscura/analyzers/digital/correlation.py +720 -0
- oscura/analyzers/digital/edges.py +632 -0
- oscura/analyzers/digital/extraction.py +413 -0
- oscura/analyzers/digital/quality.py +878 -0
- oscura/analyzers/digital/signal_quality.py +877 -0
- oscura/analyzers/digital/thresholds.py +708 -0
- oscura/analyzers/digital/timing.py +1104 -0
- oscura/analyzers/eye/__init__.py +46 -0
- oscura/analyzers/eye/diagram.py +434 -0
- oscura/analyzers/eye/metrics.py +555 -0
- oscura/analyzers/jitter/__init__.py +83 -0
- oscura/analyzers/jitter/ber.py +333 -0
- oscura/analyzers/jitter/decomposition.py +759 -0
- oscura/analyzers/jitter/measurements.py +413 -0
- oscura/analyzers/jitter/spectrum.py +220 -0
- oscura/analyzers/measurements.py +40 -0
- oscura/analyzers/packet/__init__.py +171 -0
- oscura/analyzers/packet/daq.py +1077 -0
- oscura/analyzers/packet/metrics.py +437 -0
- oscura/analyzers/packet/parser.py +327 -0
- oscura/analyzers/packet/payload.py +2156 -0
- oscura/analyzers/packet/payload_analysis.py +1312 -0
- oscura/analyzers/packet/payload_extraction.py +236 -0
- oscura/analyzers/packet/payload_patterns.py +670 -0
- oscura/analyzers/packet/stream.py +359 -0
- oscura/analyzers/patterns/__init__.py +266 -0
- oscura/analyzers/patterns/clustering.py +1036 -0
- oscura/analyzers/patterns/discovery.py +539 -0
- oscura/analyzers/patterns/learning.py +797 -0
- oscura/analyzers/patterns/matching.py +1091 -0
- oscura/analyzers/patterns/periodic.py +650 -0
- oscura/analyzers/patterns/sequences.py +767 -0
- oscura/analyzers/power/__init__.py +116 -0
- oscura/analyzers/power/ac_power.py +391 -0
- oscura/analyzers/power/basic.py +383 -0
- oscura/analyzers/power/conduction.py +314 -0
- oscura/analyzers/power/efficiency.py +297 -0
- oscura/analyzers/power/ripple.py +356 -0
- oscura/analyzers/power/soa.py +372 -0
- oscura/analyzers/power/switching.py +479 -0
- oscura/analyzers/protocol/__init__.py +150 -0
- oscura/analyzers/protocols/__init__.py +150 -0
- oscura/analyzers/protocols/base.py +500 -0
- oscura/analyzers/protocols/can.py +620 -0
- oscura/analyzers/protocols/can_fd.py +448 -0
- oscura/analyzers/protocols/flexray.py +405 -0
- oscura/analyzers/protocols/hdlc.py +399 -0
- oscura/analyzers/protocols/i2c.py +368 -0
- oscura/analyzers/protocols/i2s.py +296 -0
- oscura/analyzers/protocols/jtag.py +393 -0
- oscura/analyzers/protocols/lin.py +445 -0
- oscura/analyzers/protocols/manchester.py +333 -0
- oscura/analyzers/protocols/onewire.py +501 -0
- oscura/analyzers/protocols/spi.py +334 -0
- oscura/analyzers/protocols/swd.py +325 -0
- oscura/analyzers/protocols/uart.py +393 -0
- oscura/analyzers/protocols/usb.py +495 -0
- oscura/analyzers/signal_integrity/__init__.py +63 -0
- oscura/analyzers/signal_integrity/embedding.py +294 -0
- oscura/analyzers/signal_integrity/equalization.py +370 -0
- oscura/analyzers/signal_integrity/sparams.py +484 -0
- oscura/analyzers/spectral/__init__.py +53 -0
- oscura/analyzers/spectral/chunked.py +273 -0
- oscura/analyzers/spectral/chunked_fft.py +571 -0
- oscura/analyzers/spectral/chunked_wavelet.py +391 -0
- oscura/analyzers/spectral/fft.py +92 -0
- oscura/analyzers/statistical/__init__.py +250 -0
- oscura/analyzers/statistical/checksum.py +923 -0
- oscura/analyzers/statistical/chunked_corr.py +228 -0
- oscura/analyzers/statistical/classification.py +778 -0
- oscura/analyzers/statistical/entropy.py +1113 -0
- oscura/analyzers/statistical/ngrams.py +614 -0
- oscura/analyzers/statistics/__init__.py +119 -0
- oscura/analyzers/statistics/advanced.py +885 -0
- oscura/analyzers/statistics/basic.py +263 -0
- oscura/analyzers/statistics/correlation.py +630 -0
- oscura/analyzers/statistics/distribution.py +298 -0
- oscura/analyzers/statistics/outliers.py +463 -0
- oscura/analyzers/statistics/streaming.py +93 -0
- oscura/analyzers/statistics/trend.py +520 -0
- oscura/analyzers/validation.py +598 -0
- oscura/analyzers/waveform/__init__.py +36 -0
- oscura/analyzers/waveform/measurements.py +943 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
- oscura/analyzers/waveform/spectral.py +1689 -0
- oscura/analyzers/waveform/wavelets.py +298 -0
- oscura/api/__init__.py +62 -0
- oscura/api/dsl.py +538 -0
- oscura/api/fluent.py +571 -0
- oscura/api/operators.py +498 -0
- oscura/api/optimization.py +392 -0
- oscura/api/profiling.py +396 -0
- oscura/automotive/__init__.py +73 -0
- oscura/automotive/can/__init__.py +52 -0
- oscura/automotive/can/analysis.py +356 -0
- oscura/automotive/can/checksum.py +250 -0
- oscura/automotive/can/correlation.py +212 -0
- oscura/automotive/can/discovery.py +355 -0
- oscura/automotive/can/message_wrapper.py +375 -0
- oscura/automotive/can/models.py +385 -0
- oscura/automotive/can/patterns.py +381 -0
- oscura/automotive/can/session.py +452 -0
- oscura/automotive/can/state_machine.py +300 -0
- oscura/automotive/can/stimulus_response.py +461 -0
- oscura/automotive/dbc/__init__.py +15 -0
- oscura/automotive/dbc/generator.py +156 -0
- oscura/automotive/dbc/parser.py +146 -0
- oscura/automotive/dtc/__init__.py +30 -0
- oscura/automotive/dtc/database.py +3036 -0
- oscura/automotive/j1939/__init__.py +14 -0
- oscura/automotive/j1939/decoder.py +745 -0
- oscura/automotive/loaders/__init__.py +35 -0
- oscura/automotive/loaders/asc.py +98 -0
- oscura/automotive/loaders/blf.py +77 -0
- oscura/automotive/loaders/csv_can.py +136 -0
- oscura/automotive/loaders/dispatcher.py +136 -0
- oscura/automotive/loaders/mdf.py +331 -0
- oscura/automotive/loaders/pcap.py +132 -0
- oscura/automotive/obd/__init__.py +14 -0
- oscura/automotive/obd/decoder.py +707 -0
- oscura/automotive/uds/__init__.py +48 -0
- oscura/automotive/uds/decoder.py +265 -0
- oscura/automotive/uds/models.py +64 -0
- oscura/automotive/visualization.py +369 -0
- oscura/batch/__init__.py +55 -0
- oscura/batch/advanced.py +627 -0
- oscura/batch/aggregate.py +300 -0
- oscura/batch/analyze.py +139 -0
- oscura/batch/logging.py +487 -0
- oscura/batch/metrics.py +556 -0
- oscura/builders/__init__.py +41 -0
- oscura/builders/signal_builder.py +1131 -0
- oscura/cli/__init__.py +14 -0
- oscura/cli/batch.py +339 -0
- oscura/cli/characterize.py +273 -0
- oscura/cli/compare.py +775 -0
- oscura/cli/decode.py +551 -0
- oscura/cli/main.py +247 -0
- oscura/cli/shell.py +350 -0
- oscura/comparison/__init__.py +66 -0
- oscura/comparison/compare.py +397 -0
- oscura/comparison/golden.py +487 -0
- oscura/comparison/limits.py +391 -0
- oscura/comparison/mask.py +434 -0
- oscura/comparison/trace_diff.py +30 -0
- oscura/comparison/visualization.py +481 -0
- oscura/compliance/__init__.py +70 -0
- oscura/compliance/advanced.py +756 -0
- oscura/compliance/masks.py +363 -0
- oscura/compliance/reporting.py +483 -0
- oscura/compliance/testing.py +298 -0
- oscura/component/__init__.py +38 -0
- oscura/component/impedance.py +365 -0
- oscura/component/reactive.py +598 -0
- oscura/component/transmission_line.py +312 -0
- oscura/config/__init__.py +191 -0
- oscura/config/defaults.py +254 -0
- oscura/config/loader.py +348 -0
- oscura/config/memory.py +271 -0
- oscura/config/migration.py +458 -0
- oscura/config/pipeline.py +1077 -0
- oscura/config/preferences.py +530 -0
- oscura/config/protocol.py +875 -0
- oscura/config/schema.py +713 -0
- oscura/config/settings.py +420 -0
- oscura/config/thresholds.py +599 -0
- oscura/convenience.py +457 -0
- oscura/core/__init__.py +299 -0
- oscura/core/audit.py +457 -0
- oscura/core/backend_selector.py +405 -0
- oscura/core/cache.py +590 -0
- oscura/core/cancellation.py +439 -0
- oscura/core/confidence.py +225 -0
- oscura/core/config.py +506 -0
- oscura/core/correlation.py +216 -0
- oscura/core/cross_domain.py +422 -0
- oscura/core/debug.py +301 -0
- oscura/core/edge_cases.py +541 -0
- oscura/core/exceptions.py +535 -0
- oscura/core/gpu_backend.py +523 -0
- oscura/core/lazy.py +832 -0
- oscura/core/log_query.py +540 -0
- oscura/core/logging.py +931 -0
- oscura/core/logging_advanced.py +952 -0
- oscura/core/memoize.py +171 -0
- oscura/core/memory_check.py +274 -0
- oscura/core/memory_guard.py +290 -0
- oscura/core/memory_limits.py +336 -0
- oscura/core/memory_monitor.py +453 -0
- oscura/core/memory_progress.py +465 -0
- oscura/core/memory_warnings.py +315 -0
- oscura/core/numba_backend.py +362 -0
- oscura/core/performance.py +352 -0
- oscura/core/progress.py +524 -0
- oscura/core/provenance.py +358 -0
- oscura/core/results.py +331 -0
- oscura/core/types.py +504 -0
- oscura/core/uncertainty.py +383 -0
- oscura/discovery/__init__.py +52 -0
- oscura/discovery/anomaly_detector.py +672 -0
- oscura/discovery/auto_decoder.py +415 -0
- oscura/discovery/comparison.py +497 -0
- oscura/discovery/quality_validator.py +528 -0
- oscura/discovery/signal_detector.py +769 -0
- oscura/dsl/__init__.py +73 -0
- oscura/dsl/commands.py +246 -0
- oscura/dsl/interpreter.py +455 -0
- oscura/dsl/parser.py +689 -0
- oscura/dsl/repl.py +172 -0
- oscura/exceptions.py +59 -0
- oscura/exploratory/__init__.py +111 -0
- oscura/exploratory/error_recovery.py +642 -0
- oscura/exploratory/fuzzy.py +513 -0
- oscura/exploratory/fuzzy_advanced.py +786 -0
- oscura/exploratory/legacy.py +831 -0
- oscura/exploratory/parse.py +358 -0
- oscura/exploratory/recovery.py +275 -0
- oscura/exploratory/sync.py +382 -0
- oscura/exploratory/unknown.py +707 -0
- oscura/export/__init__.py +25 -0
- oscura/export/wireshark/README.md +265 -0
- oscura/export/wireshark/__init__.py +47 -0
- oscura/export/wireshark/generator.py +312 -0
- oscura/export/wireshark/lua_builder.py +159 -0
- oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
- oscura/export/wireshark/type_mapping.py +165 -0
- oscura/export/wireshark/validator.py +105 -0
- oscura/exporters/__init__.py +94 -0
- oscura/exporters/csv.py +303 -0
- oscura/exporters/exporters.py +44 -0
- oscura/exporters/hdf5.py +219 -0
- oscura/exporters/html_export.py +701 -0
- oscura/exporters/json_export.py +291 -0
- oscura/exporters/markdown_export.py +367 -0
- oscura/exporters/matlab_export.py +354 -0
- oscura/exporters/npz_export.py +219 -0
- oscura/exporters/spice_export.py +210 -0
- oscura/extensibility/__init__.py +131 -0
- oscura/extensibility/docs.py +752 -0
- oscura/extensibility/extensions.py +1125 -0
- oscura/extensibility/logging.py +259 -0
- oscura/extensibility/measurements.py +485 -0
- oscura/extensibility/plugins.py +414 -0
- oscura/extensibility/registry.py +346 -0
- oscura/extensibility/templates.py +913 -0
- oscura/extensibility/validation.py +651 -0
- oscura/filtering/__init__.py +89 -0
- oscura/filtering/base.py +563 -0
- oscura/filtering/convenience.py +564 -0
- oscura/filtering/design.py +725 -0
- oscura/filtering/filters.py +32 -0
- oscura/filtering/introspection.py +605 -0
- oscura/guidance/__init__.py +24 -0
- oscura/guidance/recommender.py +429 -0
- oscura/guidance/wizard.py +518 -0
- oscura/inference/__init__.py +251 -0
- oscura/inference/active_learning/README.md +153 -0
- oscura/inference/active_learning/__init__.py +38 -0
- oscura/inference/active_learning/lstar.py +257 -0
- oscura/inference/active_learning/observation_table.py +230 -0
- oscura/inference/active_learning/oracle.py +78 -0
- oscura/inference/active_learning/teachers/__init__.py +15 -0
- oscura/inference/active_learning/teachers/simulator.py +192 -0
- oscura/inference/adaptive_tuning.py +453 -0
- oscura/inference/alignment.py +653 -0
- oscura/inference/bayesian.py +943 -0
- oscura/inference/binary.py +1016 -0
- oscura/inference/crc_reverse.py +711 -0
- oscura/inference/logic.py +288 -0
- oscura/inference/message_format.py +1305 -0
- oscura/inference/protocol.py +417 -0
- oscura/inference/protocol_dsl.py +1084 -0
- oscura/inference/protocol_library.py +1230 -0
- oscura/inference/sequences.py +809 -0
- oscura/inference/signal_intelligence.py +1509 -0
- oscura/inference/spectral.py +215 -0
- oscura/inference/state_machine.py +634 -0
- oscura/inference/stream.py +918 -0
- oscura/integrations/__init__.py +59 -0
- oscura/integrations/llm.py +1827 -0
- oscura/jupyter/__init__.py +32 -0
- oscura/jupyter/display.py +268 -0
- oscura/jupyter/magic.py +334 -0
- oscura/loaders/__init__.py +526 -0
- oscura/loaders/binary.py +69 -0
- oscura/loaders/configurable.py +1255 -0
- oscura/loaders/csv.py +26 -0
- oscura/loaders/csv_loader.py +473 -0
- oscura/loaders/hdf5.py +9 -0
- oscura/loaders/hdf5_loader.py +510 -0
- oscura/loaders/lazy.py +370 -0
- oscura/loaders/mmap_loader.py +583 -0
- oscura/loaders/numpy_loader.py +436 -0
- oscura/loaders/pcap.py +432 -0
- oscura/loaders/preprocessing.py +368 -0
- oscura/loaders/rigol.py +287 -0
- oscura/loaders/sigrok.py +321 -0
- oscura/loaders/tdms.py +367 -0
- oscura/loaders/tektronix.py +711 -0
- oscura/loaders/validation.py +584 -0
- oscura/loaders/vcd.py +464 -0
- oscura/loaders/wav.py +233 -0
- oscura/math/__init__.py +45 -0
- oscura/math/arithmetic.py +824 -0
- oscura/math/interpolation.py +413 -0
- oscura/onboarding/__init__.py +39 -0
- oscura/onboarding/help.py +498 -0
- oscura/onboarding/tutorials.py +405 -0
- oscura/onboarding/wizard.py +466 -0
- oscura/optimization/__init__.py +19 -0
- oscura/optimization/parallel.py +440 -0
- oscura/optimization/search.py +532 -0
- oscura/pipeline/__init__.py +43 -0
- oscura/pipeline/base.py +338 -0
- oscura/pipeline/composition.py +242 -0
- oscura/pipeline/parallel.py +448 -0
- oscura/pipeline/pipeline.py +375 -0
- oscura/pipeline/reverse_engineering.py +1119 -0
- oscura/plugins/__init__.py +122 -0
- oscura/plugins/base.py +272 -0
- oscura/plugins/cli.py +497 -0
- oscura/plugins/discovery.py +411 -0
- oscura/plugins/isolation.py +418 -0
- oscura/plugins/lifecycle.py +959 -0
- oscura/plugins/manager.py +493 -0
- oscura/plugins/registry.py +421 -0
- oscura/plugins/versioning.py +372 -0
- oscura/py.typed +0 -0
- oscura/quality/__init__.py +65 -0
- oscura/quality/ensemble.py +740 -0
- oscura/quality/explainer.py +338 -0
- oscura/quality/scoring.py +616 -0
- oscura/quality/warnings.py +456 -0
- oscura/reporting/__init__.py +248 -0
- oscura/reporting/advanced.py +1234 -0
- oscura/reporting/analyze.py +448 -0
- oscura/reporting/argument_preparer.py +596 -0
- oscura/reporting/auto_report.py +507 -0
- oscura/reporting/batch.py +615 -0
- oscura/reporting/chart_selection.py +223 -0
- oscura/reporting/comparison.py +330 -0
- oscura/reporting/config.py +615 -0
- oscura/reporting/content/__init__.py +39 -0
- oscura/reporting/content/executive.py +127 -0
- oscura/reporting/content/filtering.py +191 -0
- oscura/reporting/content/minimal.py +257 -0
- oscura/reporting/content/verbosity.py +162 -0
- oscura/reporting/core.py +508 -0
- oscura/reporting/core_formats/__init__.py +17 -0
- oscura/reporting/core_formats/multi_format.py +210 -0
- oscura/reporting/engine.py +836 -0
- oscura/reporting/export.py +366 -0
- oscura/reporting/formatting/__init__.py +129 -0
- oscura/reporting/formatting/emphasis.py +81 -0
- oscura/reporting/formatting/numbers.py +403 -0
- oscura/reporting/formatting/standards.py +55 -0
- oscura/reporting/formatting.py +466 -0
- oscura/reporting/html.py +578 -0
- oscura/reporting/index.py +590 -0
- oscura/reporting/multichannel.py +296 -0
- oscura/reporting/output.py +379 -0
- oscura/reporting/pdf.py +373 -0
- oscura/reporting/plots.py +731 -0
- oscura/reporting/pptx_export.py +360 -0
- oscura/reporting/renderers/__init__.py +11 -0
- oscura/reporting/renderers/pdf.py +94 -0
- oscura/reporting/sections.py +471 -0
- oscura/reporting/standards.py +680 -0
- oscura/reporting/summary_generator.py +368 -0
- oscura/reporting/tables.py +397 -0
- oscura/reporting/template_system.py +724 -0
- oscura/reporting/templates/__init__.py +15 -0
- oscura/reporting/templates/definition.py +205 -0
- oscura/reporting/templates/index.html +649 -0
- oscura/reporting/templates/index.md +173 -0
- oscura/schemas/__init__.py +158 -0
- oscura/schemas/bus_configuration.json +322 -0
- oscura/schemas/device_mapping.json +182 -0
- oscura/schemas/packet_format.json +418 -0
- oscura/schemas/protocol_definition.json +363 -0
- oscura/search/__init__.py +16 -0
- oscura/search/anomaly.py +292 -0
- oscura/search/context.py +149 -0
- oscura/search/pattern.py +160 -0
- oscura/session/__init__.py +34 -0
- oscura/session/annotations.py +289 -0
- oscura/session/history.py +313 -0
- oscura/session/session.py +445 -0
- oscura/streaming/__init__.py +43 -0
- oscura/streaming/chunked.py +611 -0
- oscura/streaming/progressive.py +393 -0
- oscura/streaming/realtime.py +622 -0
- oscura/testing/__init__.py +54 -0
- oscura/testing/synthetic.py +808 -0
- oscura/triggering/__init__.py +68 -0
- oscura/triggering/base.py +229 -0
- oscura/triggering/edge.py +353 -0
- oscura/triggering/pattern.py +344 -0
- oscura/triggering/pulse.py +581 -0
- oscura/triggering/window.py +453 -0
- oscura/ui/__init__.py +48 -0
- oscura/ui/formatters.py +526 -0
- oscura/ui/progressive_display.py +340 -0
- oscura/utils/__init__.py +99 -0
- oscura/utils/autodetect.py +338 -0
- oscura/utils/buffer.py +389 -0
- oscura/utils/lazy.py +407 -0
- oscura/utils/lazy_imports.py +147 -0
- oscura/utils/memory.py +836 -0
- oscura/utils/memory_advanced.py +1326 -0
- oscura/utils/memory_extensions.py +465 -0
- oscura/utils/progressive.py +352 -0
- oscura/utils/windowing.py +362 -0
- oscura/visualization/__init__.py +321 -0
- oscura/visualization/accessibility.py +526 -0
- oscura/visualization/annotations.py +374 -0
- oscura/visualization/axis_scaling.py +305 -0
- oscura/visualization/colors.py +453 -0
- oscura/visualization/digital.py +337 -0
- oscura/visualization/eye.py +420 -0
- oscura/visualization/histogram.py +281 -0
- oscura/visualization/interactive.py +858 -0
- oscura/visualization/jitter.py +702 -0
- oscura/visualization/keyboard.py +394 -0
- oscura/visualization/layout.py +365 -0
- oscura/visualization/optimization.py +1028 -0
- oscura/visualization/palettes.py +446 -0
- oscura/visualization/plot.py +92 -0
- oscura/visualization/power.py +290 -0
- oscura/visualization/power_extended.py +626 -0
- oscura/visualization/presets.py +467 -0
- oscura/visualization/protocols.py +932 -0
- oscura/visualization/render.py +207 -0
- oscura/visualization/rendering.py +444 -0
- oscura/visualization/reverse_engineering.py +791 -0
- oscura/visualization/signal_integrity.py +808 -0
- oscura/visualization/specialized.py +553 -0
- oscura/visualization/spectral.py +811 -0
- oscura/visualization/styles.py +381 -0
- oscura/visualization/thumbnails.py +311 -0
- oscura/visualization/time_axis.py +351 -0
- oscura/visualization/waveform.py +367 -0
- oscura/workflow/__init__.py +13 -0
- oscura/workflow/dag.py +377 -0
- oscura/workflows/__init__.py +58 -0
- oscura/workflows/compliance.py +280 -0
- oscura/workflows/digital.py +272 -0
- oscura/workflows/multi_trace.py +502 -0
- oscura/workflows/power.py +178 -0
- oscura/workflows/protocol.py +492 -0
- oscura/workflows/reverse_engineering.py +639 -0
- oscura/workflows/signal_integrity.py +227 -0
- oscura-0.1.1.dist-info/METADATA +300 -0
- oscura-0.1.1.dist-info/RECORD +463 -0
- oscura-0.1.1.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/licenses/LICENSE +1 -1
- oscura-0.0.1.dist-info/METADATA +0 -63
- oscura-0.0.1.dist-info/RECORD +0 -5
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
"""Protocol definition registry and loading.
|
|
2
|
+
|
|
3
|
+
This module provides protocol definition management including registry,
|
|
4
|
+
loading from YAML/JSON files, inheritance, hot reload support, version
|
|
5
|
+
migration, and circular dependency detection.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import contextlib
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
from oscura.config.schema import validate_against_schema
|
|
22
|
+
from oscura.core.exceptions import ConfigurationError
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ProtocolDefinition:
|
|
32
|
+
"""Protocol definition with metadata and configuration.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
name: Protocol identifier (e.g., "uart", "spi")
|
|
36
|
+
version: Protocol version (semver)
|
|
37
|
+
description: Human-readable description
|
|
38
|
+
author: Protocol definition author
|
|
39
|
+
timing: Timing configuration (baud rates, data bits, etc.)
|
|
40
|
+
voltage_levels: Logic level configuration
|
|
41
|
+
state_machine: Protocol state machine definition
|
|
42
|
+
extends: Parent protocol name for inheritance
|
|
43
|
+
metadata: Additional custom metadata
|
|
44
|
+
source_file: Path to source file (for hot reload)
|
|
45
|
+
schema_version: Schema version for migration support
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
>>> protocol = ProtocolDefinition(
|
|
49
|
+
... name="uart",
|
|
50
|
+
... version="1.0.0",
|
|
51
|
+
... timing={"baud_rates": [9600, 115200]}
|
|
52
|
+
... )
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
name: str
|
|
56
|
+
version: str = "1.0.0"
|
|
57
|
+
description: str = ""
|
|
58
|
+
author: str = ""
|
|
59
|
+
timing: dict[str, Any] = field(default_factory=dict)
|
|
60
|
+
voltage_levels: dict[str, Any] = field(default_factory=dict)
|
|
61
|
+
state_machine: dict[str, Any] = field(default_factory=dict)
|
|
62
|
+
extends: str | None = None
|
|
63
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
64
|
+
source_file: str | None = None
|
|
65
|
+
schema_version: str = "1.0.0"
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def supports_digital(self) -> bool:
|
|
69
|
+
"""Check if protocol supports digital signals."""
|
|
70
|
+
return True # Most protocols are digital
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def supports_analog(self) -> bool:
|
|
74
|
+
"""Check if protocol requires analog threshold detection."""
|
|
75
|
+
return bool(self.voltage_levels)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def sample_rate_min(self) -> float:
|
|
79
|
+
"""Minimum sample rate required for decoding."""
|
|
80
|
+
# Estimate from baud rate (need 10x oversampling typically)
|
|
81
|
+
baud_rates = self.timing.get("baud_rates", [])
|
|
82
|
+
if baud_rates:
|
|
83
|
+
max_baud = max(baud_rates)
|
|
84
|
+
return float(max_baud * 10)
|
|
85
|
+
return 1e6 # Default 1 MHz
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def sample_rate_max(self) -> float | None:
|
|
89
|
+
"""Maximum useful sample rate for decoding."""
|
|
90
|
+
return None # No upper limit typically
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def bit_widths(self) -> list[int]:
|
|
94
|
+
"""Supported data bit widths."""
|
|
95
|
+
return self.timing.get("data_bits", [8]) # type: ignore[no-any-return]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class ProtocolCapabilities:
|
|
100
|
+
"""Protocol capabilities for querying and filtering.
|
|
101
|
+
|
|
102
|
+
Attributes:
|
|
103
|
+
supports_digital: Whether protocol uses digital signals
|
|
104
|
+
supports_analog: Whether protocol needs analog thresholds
|
|
105
|
+
sample_rate_min: Minimum required sample rate (Hz)
|
|
106
|
+
sample_rate_max: Maximum useful sample rate (Hz)
|
|
107
|
+
bit_widths: Supported data widths
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
supports_digital: bool = True
|
|
111
|
+
supports_analog: bool = False
|
|
112
|
+
sample_rate_min: float = 1e6
|
|
113
|
+
sample_rate_max: float | None = None
|
|
114
|
+
bit_widths: list[int] = field(default_factory=lambda: [8])
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ProtocolRegistry:
|
|
118
|
+
"""Central registry of all protocol definitions.
|
|
119
|
+
|
|
120
|
+
Provides O(1) lookup by name, version queries, capability filtering,
|
|
121
|
+
and enumeration for UI integration.
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
>>> registry = ProtocolRegistry()
|
|
125
|
+
>>> uart = registry.get("uart")
|
|
126
|
+
>>> i2c = registry.get("i2c", version="2.1.0")
|
|
127
|
+
>>> all_protocols = registry.list()
|
|
128
|
+
>>> digital = registry.filter(supports_digital=True)
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
_instance: ProtocolRegistry | None = None
|
|
132
|
+
|
|
133
|
+
def __new__(cls) -> ProtocolRegistry:
|
|
134
|
+
"""Ensure singleton instance."""
|
|
135
|
+
if cls._instance is None:
|
|
136
|
+
cls._instance = super().__new__(cls)
|
|
137
|
+
cls._instance._protocols: dict[str, dict[str, ProtocolDefinition]] = {} # type: ignore[misc, attr-defined]
|
|
138
|
+
cls._instance._default_versions: dict[str, str] = {} # type: ignore[misc, attr-defined]
|
|
139
|
+
cls._instance._watchers: list[Callable[[ProtocolDefinition], None]] = [] # type: ignore[misc, attr-defined]
|
|
140
|
+
return cls._instance
|
|
141
|
+
|
|
142
|
+
def register(
|
|
143
|
+
self,
|
|
144
|
+
protocol: ProtocolDefinition,
|
|
145
|
+
*,
|
|
146
|
+
set_default: bool = True,
|
|
147
|
+
overwrite: bool = False,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Register a protocol definition.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
protocol: Protocol definition to register
|
|
153
|
+
set_default: If True, set as default version
|
|
154
|
+
overwrite: If True, allow overwriting existing registration
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
ValueError: If protocol already registered and overwrite=False
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
>>> registry.register(uart_protocol)
|
|
161
|
+
"""
|
|
162
|
+
if protocol.name not in self._protocols: # type: ignore[attr-defined]
|
|
163
|
+
self._protocols[protocol.name] = {} # type: ignore[attr-defined]
|
|
164
|
+
|
|
165
|
+
if protocol.version in self._protocols[protocol.name] and not overwrite: # type: ignore[attr-defined]
|
|
166
|
+
raise ValueError(f"Protocol '{protocol.name}' v{protocol.version} already registered")
|
|
167
|
+
|
|
168
|
+
self._protocols[protocol.name][protocol.version] = protocol # type: ignore[attr-defined]
|
|
169
|
+
|
|
170
|
+
if set_default:
|
|
171
|
+
self._default_versions[protocol.name] = protocol.version # type: ignore[attr-defined]
|
|
172
|
+
|
|
173
|
+
logger.debug(f"Registered protocol: {protocol.name} v{protocol.version}")
|
|
174
|
+
|
|
175
|
+
def get(self, name: str, version: str | None = None) -> ProtocolDefinition:
|
|
176
|
+
"""Get protocol by name and optional version.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
name: Protocol name
|
|
180
|
+
version: Specific version or None for default
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Protocol definition
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
KeyError: If protocol not found
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
>>> uart = registry.get("uart")
|
|
190
|
+
>>> i2c = registry.get("i2c", version="2.1.0")
|
|
191
|
+
"""
|
|
192
|
+
if name not in self._protocols: # type: ignore[attr-defined]
|
|
193
|
+
raise KeyError(
|
|
194
|
+
f"Protocol '{name}' not found. Available: {list(self._protocols.keys())}" # type: ignore[attr-defined]
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if version is None:
|
|
198
|
+
version = self._default_versions.get(name) # type: ignore[attr-defined]
|
|
199
|
+
if version is None:
|
|
200
|
+
# Get latest version
|
|
201
|
+
versions = sorted(self._protocols[name].keys()) # type: ignore[attr-defined]
|
|
202
|
+
version = versions[-1] if versions else None
|
|
203
|
+
|
|
204
|
+
if version is None or version not in self._protocols[name]: # type: ignore[attr-defined]
|
|
205
|
+
raise KeyError(
|
|
206
|
+
f"Protocol '{name}' version '{version}' not found. "
|
|
207
|
+
f"Available versions: {list(self._protocols[name].keys())}" # type: ignore[attr-defined]
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return self._protocols[name][version] # type: ignore[no-any-return, attr-defined]
|
|
211
|
+
|
|
212
|
+
def list(self) -> list[ProtocolDefinition]:
|
|
213
|
+
"""List all available protocols (default versions).
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Sorted list of protocol definitions
|
|
217
|
+
|
|
218
|
+
Example:
|
|
219
|
+
>>> for proto in registry.list():
|
|
220
|
+
... print(f"{proto.name} v{proto.version}: {proto.description}")
|
|
221
|
+
"""
|
|
222
|
+
protocols = []
|
|
223
|
+
for name in sorted(self._protocols.keys()): # type: ignore[attr-defined]
|
|
224
|
+
version = self._default_versions.get(name) # type: ignore[attr-defined]
|
|
225
|
+
if version and version in self._protocols[name]: # type: ignore[attr-defined]
|
|
226
|
+
protocols.append(self._protocols[name][version]) # type: ignore[attr-defined]
|
|
227
|
+
elif self._protocols[name]: # type: ignore[attr-defined]
|
|
228
|
+
# Get latest version
|
|
229
|
+
latest = sorted(self._protocols[name].keys())[-1] # type: ignore[attr-defined]
|
|
230
|
+
protocols.append(self._protocols[name][latest]) # type: ignore[attr-defined]
|
|
231
|
+
return protocols
|
|
232
|
+
|
|
233
|
+
def get_capabilities(self, name: str) -> ProtocolCapabilities:
|
|
234
|
+
"""Query protocol capabilities.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
name: Protocol name
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Protocol capabilities
|
|
241
|
+
|
|
242
|
+
Example:
|
|
243
|
+
>>> caps = registry.get_capabilities("uart")
|
|
244
|
+
>>> print(f"Sample rate: {caps.sample_rate_min}-{caps.sample_rate_max} Hz")
|
|
245
|
+
"""
|
|
246
|
+
protocol = self.get(name)
|
|
247
|
+
return ProtocolCapabilities(
|
|
248
|
+
supports_digital=protocol.supports_digital,
|
|
249
|
+
supports_analog=protocol.supports_analog,
|
|
250
|
+
sample_rate_min=protocol.sample_rate_min,
|
|
251
|
+
sample_rate_max=protocol.sample_rate_max,
|
|
252
|
+
bit_widths=protocol.bit_widths,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def filter(
|
|
256
|
+
self,
|
|
257
|
+
supports_digital: bool | None = None,
|
|
258
|
+
supports_analog: bool | None = None,
|
|
259
|
+
sample_rate_min__gte: float | None = None,
|
|
260
|
+
sample_rate_max__lte: float | None = None,
|
|
261
|
+
) -> list[ProtocolDefinition]: # type: ignore[valid-type]
|
|
262
|
+
"""Filter protocols by capabilities.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
supports_digital: Filter by digital support
|
|
266
|
+
supports_analog: Filter by analog support
|
|
267
|
+
sample_rate_min__gte: Minimum sample rate >= value
|
|
268
|
+
sample_rate_max__lte: Maximum sample rate <= value
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
List of matching protocols
|
|
272
|
+
|
|
273
|
+
Example:
|
|
274
|
+
>>> digital = registry.filter(supports_digital=True)
|
|
275
|
+
>>> high_speed = registry.filter(sample_rate_min__gte=1_000_000)
|
|
276
|
+
"""
|
|
277
|
+
results = []
|
|
278
|
+
for protocol in self.list():
|
|
279
|
+
match = True
|
|
280
|
+
|
|
281
|
+
if supports_digital is not None:
|
|
282
|
+
if protocol.supports_digital != supports_digital:
|
|
283
|
+
match = False
|
|
284
|
+
|
|
285
|
+
if supports_analog is not None:
|
|
286
|
+
if protocol.supports_analog != supports_analog:
|
|
287
|
+
match = False
|
|
288
|
+
|
|
289
|
+
if sample_rate_min__gte is not None:
|
|
290
|
+
if protocol.sample_rate_min < sample_rate_min__gte:
|
|
291
|
+
match = False
|
|
292
|
+
|
|
293
|
+
if sample_rate_max__lte is not None and (
|
|
294
|
+
protocol.sample_rate_max and protocol.sample_rate_max > sample_rate_max__lte
|
|
295
|
+
):
|
|
296
|
+
match = False
|
|
297
|
+
|
|
298
|
+
if match:
|
|
299
|
+
results.append(protocol)
|
|
300
|
+
|
|
301
|
+
return results
|
|
302
|
+
|
|
303
|
+
def has_protocol(self, name: str, version: str | None = None) -> bool:
|
|
304
|
+
"""Check if protocol is registered.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
name: Protocol name
|
|
308
|
+
version: Specific version or None for any
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
True if registered
|
|
312
|
+
"""
|
|
313
|
+
if name not in self._protocols: # type: ignore[attr-defined]
|
|
314
|
+
return False
|
|
315
|
+
if version is None:
|
|
316
|
+
return True
|
|
317
|
+
return version in self._protocols[name] # type: ignore[attr-defined]
|
|
318
|
+
|
|
319
|
+
def list_versions(self, name: str) -> list[str]: # type: ignore[valid-type]
|
|
320
|
+
"""List all versions of a protocol.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
name: Protocol name
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
List of version strings
|
|
327
|
+
"""
|
|
328
|
+
if name not in self._protocols: # type: ignore[attr-defined]
|
|
329
|
+
return []
|
|
330
|
+
return sorted(self._protocols[name].keys()) # type: ignore[attr-defined]
|
|
331
|
+
|
|
332
|
+
def on_change(self, callback: Callable[[ProtocolDefinition], None]) -> None:
|
|
333
|
+
"""Register callback for protocol changes (hot reload support).
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
callback: Function to call when protocol is reloaded
|
|
337
|
+
|
|
338
|
+
Example:
|
|
339
|
+
>>> watcher = registry.on_change(lambda proto: print(f"Reloaded {proto.name}"))
|
|
340
|
+
"""
|
|
341
|
+
self._watchers.append(callback) # type: ignore[attr-defined]
|
|
342
|
+
|
|
343
|
+
def _notify_change(self, protocol: ProtocolDefinition) -> None:
|
|
344
|
+
"""Notify watchers of protocol change."""
|
|
345
|
+
for callback in self._watchers: # type: ignore[attr-defined]
|
|
346
|
+
try:
|
|
347
|
+
callback(protocol)
|
|
348
|
+
except Exception as e:
|
|
349
|
+
logger.warning(f"Protocol change callback failed: {e}")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def load_protocol(path: str | Path, validate: bool = True) -> ProtocolDefinition:
|
|
353
|
+
"""Load protocol definition from YAML or JSON file.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
path: Path to protocol definition file
|
|
357
|
+
validate: If True, validate against schema
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Loaded protocol definition
|
|
361
|
+
|
|
362
|
+
Raises:
|
|
363
|
+
ConfigurationError: If file invalid or validation fails
|
|
364
|
+
|
|
365
|
+
Example:
|
|
366
|
+
>>> protocol = load_protocol("configs/uart.yaml")
|
|
367
|
+
>>> protocol = load_protocol("configs/i2c.json")
|
|
368
|
+
"""
|
|
369
|
+
path = Path(path)
|
|
370
|
+
|
|
371
|
+
if not path.exists():
|
|
372
|
+
raise ConfigurationError(
|
|
373
|
+
f"Protocol definition file not found: {path.name}", details=f"File path: {path}"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
with open(path, encoding="utf-8") as f:
|
|
378
|
+
content = f.read()
|
|
379
|
+
if path.suffix in (".yaml", ".yml"):
|
|
380
|
+
data = yaml.safe_load(content)
|
|
381
|
+
else:
|
|
382
|
+
import json
|
|
383
|
+
|
|
384
|
+
data = json.loads(content)
|
|
385
|
+
|
|
386
|
+
except yaml.YAMLError as e:
|
|
387
|
+
raise ConfigurationError(
|
|
388
|
+
f"YAML parse error in {path.name}", details=f"File: {path}\nError: {e}"
|
|
389
|
+
) from e
|
|
390
|
+
except Exception as e:
|
|
391
|
+
raise ConfigurationError(
|
|
392
|
+
f"Failed to load protocol file: {path.name}", details=f"File: {path}\nError: {e}"
|
|
393
|
+
) from e
|
|
394
|
+
|
|
395
|
+
# Handle nested 'protocol' key
|
|
396
|
+
if "protocol" in data:
|
|
397
|
+
data = data["protocol"]
|
|
398
|
+
|
|
399
|
+
if validate:
|
|
400
|
+
try:
|
|
401
|
+
validate_against_schema(data, "protocol")
|
|
402
|
+
except Exception as e:
|
|
403
|
+
raise ConfigurationError(
|
|
404
|
+
f"Protocol validation failed for {path.name}",
|
|
405
|
+
details=f"File: {path}\nError: {e}",
|
|
406
|
+
) from e
|
|
407
|
+
|
|
408
|
+
protocol = ProtocolDefinition(
|
|
409
|
+
name=data.get("name", path.stem),
|
|
410
|
+
version=data.get("version", "1.0.0"),
|
|
411
|
+
description=data.get("description", ""),
|
|
412
|
+
author=data.get("author", ""),
|
|
413
|
+
timing=data.get("timing", {}),
|
|
414
|
+
voltage_levels=data.get("voltage_levels", {}),
|
|
415
|
+
state_machine=data.get("state_machine", {}),
|
|
416
|
+
extends=data.get("extends"),
|
|
417
|
+
metadata=data.get("metadata", {}),
|
|
418
|
+
source_file=str(path),
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
logger.info(f"Loaded protocol: {protocol.name} v{protocol.version} from {path}")
|
|
422
|
+
return protocol
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def resolve_inheritance(
|
|
426
|
+
protocol: ProtocolDefinition,
|
|
427
|
+
registry: ProtocolRegistry,
|
|
428
|
+
*,
|
|
429
|
+
max_depth: int = 5,
|
|
430
|
+
deep_merge: bool = False,
|
|
431
|
+
_visited: set[str] | None = None,
|
|
432
|
+
) -> ProtocolDefinition:
|
|
433
|
+
"""Resolve protocol inheritance chain with circular detection.
|
|
434
|
+
|
|
435
|
+
Supports multi-level inheritance (up to 5 levels deep) with both
|
|
436
|
+
shallow and deep merge strategies for nested properties.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
protocol: Protocol with potential inheritance
|
|
440
|
+
registry: Registry to look up parent protocols
|
|
441
|
+
max_depth: Maximum inheritance depth (default 5.)
|
|
442
|
+
deep_merge: If True, recursively merge nested dicts; else shallow merge
|
|
443
|
+
_visited: Set of visited protocols for cycle detection
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
Protocol with inherited properties merged
|
|
447
|
+
|
|
448
|
+
Raises:
|
|
449
|
+
ConfigurationError: If circular inheritance or depth exceeded
|
|
450
|
+
|
|
451
|
+
Example:
|
|
452
|
+
>>> resolved = resolve_inheritance(spi_variant, registry)
|
|
453
|
+
>>> resolved_deep = resolve_inheritance(spi_variant, registry, deep_merge=True)
|
|
454
|
+
"""
|
|
455
|
+
if _visited is None:
|
|
456
|
+
_visited = set()
|
|
457
|
+
|
|
458
|
+
if not protocol.extends:
|
|
459
|
+
return protocol
|
|
460
|
+
|
|
461
|
+
# Cycle detection using DFS with visited set
|
|
462
|
+
if protocol.name in _visited:
|
|
463
|
+
cycle_list = [*list(_visited), protocol.name]
|
|
464
|
+
cycle = " → ".join(cycle_list)
|
|
465
|
+
raise ConfigurationError(
|
|
466
|
+
f"Circular inheritance detected: {cycle}",
|
|
467
|
+
details=f"Protocol inheritance forms a cycle. Remove 'extends' from one of: {', '.join(cycle_list)}",
|
|
468
|
+
fix_hint=f"Break the cycle by removing the 'extends' field from {protocol.name}",
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
# Depth limit check
|
|
472
|
+
if len(_visited) >= max_depth:
|
|
473
|
+
chain = " → ".join([*list(_visited), protocol.name])
|
|
474
|
+
raise ConfigurationError(
|
|
475
|
+
f"Inheritance depth exceeded maximum of {max_depth}",
|
|
476
|
+
details=f"Current chain: {chain}",
|
|
477
|
+
fix_hint="Flatten the inheritance hierarchy or increase max_depth",
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
_visited.add(protocol.name)
|
|
481
|
+
|
|
482
|
+
# Get parent protocol
|
|
483
|
+
try:
|
|
484
|
+
parent = registry.get(protocol.extends)
|
|
485
|
+
except KeyError as e:
|
|
486
|
+
available = ", ".join(registry._protocols.keys()) # type: ignore[attr-defined]
|
|
487
|
+
raise ConfigurationError(
|
|
488
|
+
f"Parent protocol '{protocol.extends}' not found",
|
|
489
|
+
details=f"Protocol '{protocol.name}' extends missing parent. Available: {available}",
|
|
490
|
+
fix_hint=f"Add protocol '{protocol.extends}' to registry or fix 'extends' field",
|
|
491
|
+
) from e
|
|
492
|
+
|
|
493
|
+
# Recursively resolve parent
|
|
494
|
+
resolved_parent = resolve_inheritance(
|
|
495
|
+
parent, registry, max_depth=max_depth, deep_merge=deep_merge, _visited=_visited
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# Merge properties (child overrides parent)
|
|
499
|
+
if deep_merge:
|
|
500
|
+
merged_timing = _deep_merge_dicts(resolved_parent.timing, protocol.timing)
|
|
501
|
+
merged_voltage = _deep_merge_dicts(resolved_parent.voltage_levels, protocol.voltage_levels)
|
|
502
|
+
merged_state = _deep_merge_dicts(resolved_parent.state_machine, protocol.state_machine)
|
|
503
|
+
merged_metadata = _deep_merge_dicts(resolved_parent.metadata, protocol.metadata)
|
|
504
|
+
else:
|
|
505
|
+
# Shallow merge (default)
|
|
506
|
+
merged_timing = {**resolved_parent.timing, **protocol.timing}
|
|
507
|
+
merged_voltage = {**resolved_parent.voltage_levels, **protocol.voltage_levels}
|
|
508
|
+
merged_state = {**resolved_parent.state_machine, **protocol.state_machine}
|
|
509
|
+
merged_metadata = {**resolved_parent.metadata, **protocol.metadata}
|
|
510
|
+
|
|
511
|
+
return ProtocolDefinition(
|
|
512
|
+
name=protocol.name,
|
|
513
|
+
version=protocol.version,
|
|
514
|
+
description=protocol.description or resolved_parent.description,
|
|
515
|
+
author=protocol.author or resolved_parent.author,
|
|
516
|
+
timing=merged_timing,
|
|
517
|
+
voltage_levels=merged_voltage,
|
|
518
|
+
state_machine=merged_state,
|
|
519
|
+
extends=None, # Clear extends after resolution
|
|
520
|
+
metadata=merged_metadata,
|
|
521
|
+
source_file=protocol.source_file,
|
|
522
|
+
schema_version=protocol.schema_version,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _deep_merge_dicts(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
|
527
|
+
"""Deep merge two dictionaries recursively.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
base: Base dictionary
|
|
531
|
+
override: Override dictionary (takes precedence)
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
Merged dictionary
|
|
535
|
+
|
|
536
|
+
Example:
|
|
537
|
+
>>> base = {"a": {"b": 1, "c": 2}}
|
|
538
|
+
>>> override = {"a": {"c": 3, "d": 4}}
|
|
539
|
+
>>> _deep_merge_dicts(base, override)
|
|
540
|
+
{'a': {'b': 1, 'c': 3, 'd': 4}}
|
|
541
|
+
"""
|
|
542
|
+
result = base.copy()
|
|
543
|
+
for key, value in override.items():
|
|
544
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
545
|
+
result[key] = _deep_merge_dicts(result[key], value)
|
|
546
|
+
else:
|
|
547
|
+
result[key] = value
|
|
548
|
+
return result
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
class ProtocolWatcher:
|
|
552
|
+
"""File watcher for hot-reloading protocol definitions.
|
|
553
|
+
|
|
554
|
+
Monitors a directory for protocol file changes and reloads
|
|
555
|
+
automatically with <2s latency using background thread polling.
|
|
556
|
+
|
|
557
|
+
Example:
|
|
558
|
+
>>> watcher = ProtocolWatcher("configs/")
|
|
559
|
+
>>> watcher.on_change(lambda proto: print(f"Reloaded {proto.name}"))
|
|
560
|
+
>>> watcher.start()
|
|
561
|
+
>>> # ... later ...
|
|
562
|
+
>>> watcher.stop()
|
|
563
|
+
"""
|
|
564
|
+
|
|
565
|
+
def __init__(
|
|
566
|
+
self,
|
|
567
|
+
directory: str | Path,
|
|
568
|
+
*,
|
|
569
|
+
poll_interval: float = 1.0,
|
|
570
|
+
registry: ProtocolRegistry | None = None,
|
|
571
|
+
):
|
|
572
|
+
"""Initialize watcher for directory.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
directory: Directory to watch for protocol files
|
|
576
|
+
poll_interval: Polling interval in seconds (default 1.0 for <2s latency)
|
|
577
|
+
registry: Registry to auto-register reloaded protocols
|
|
578
|
+
"""
|
|
579
|
+
self.directory = Path(directory)
|
|
580
|
+
self.poll_interval = poll_interval
|
|
581
|
+
self.registry = registry
|
|
582
|
+
self._callbacks: list[Callable[[ProtocolDefinition], None]] = []
|
|
583
|
+
self._running = False
|
|
584
|
+
self._thread: threading.Thread | None = None
|
|
585
|
+
self._file_mtimes: dict[str, float] = {}
|
|
586
|
+
|
|
587
|
+
def on_change(self, callback: Callable[[ProtocolDefinition], None]) -> None:
|
|
588
|
+
"""Register callback for protocol changes.
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
callback: Function to call with reloaded protocol
|
|
592
|
+
"""
|
|
593
|
+
self._callbacks.append(callback)
|
|
594
|
+
|
|
595
|
+
def start(self) -> None:
|
|
596
|
+
"""Start watching for file changes in background thread.
|
|
597
|
+
|
|
598
|
+
The watcher polls the directory every poll_interval seconds,
|
|
599
|
+
ensuring <2s latency for detecting changes.
|
|
600
|
+
"""
|
|
601
|
+
if self._running:
|
|
602
|
+
logger.warning("Protocol watcher already running")
|
|
603
|
+
return
|
|
604
|
+
|
|
605
|
+
self._running = True
|
|
606
|
+
self._scan_files()
|
|
607
|
+
|
|
608
|
+
# Start background polling thread
|
|
609
|
+
self._thread = threading.Thread(target=self._watch_loop, daemon=True)
|
|
610
|
+
self._thread.start()
|
|
611
|
+
|
|
612
|
+
logger.info(
|
|
613
|
+
f"Started watching protocols in {self.directory} (poll interval: {self.poll_interval}s)"
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
def stop(self) -> None:
|
|
617
|
+
"""Stop watching for file changes."""
|
|
618
|
+
self._running = False
|
|
619
|
+
if self._thread and self._thread.is_alive():
|
|
620
|
+
self._thread.join(timeout=2.0)
|
|
621
|
+
logger.info("Stopped protocol watcher")
|
|
622
|
+
|
|
623
|
+
def _watch_loop(self) -> None:
|
|
624
|
+
"""Background thread polling loop."""
|
|
625
|
+
while self._running:
|
|
626
|
+
try:
|
|
627
|
+
self.check_changes()
|
|
628
|
+
except Exception as e:
|
|
629
|
+
logger.error(f"Error in protocol watcher: {e}")
|
|
630
|
+
time.sleep(self.poll_interval)
|
|
631
|
+
|
|
632
|
+
def check_changes(self) -> list[ProtocolDefinition]:
|
|
633
|
+
"""Check for changed files and reload.
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
List of reloaded protocols
|
|
637
|
+
"""
|
|
638
|
+
if not self._running:
|
|
639
|
+
return []
|
|
640
|
+
|
|
641
|
+
reloaded = []
|
|
642
|
+
for file_path in self.directory.glob("**/*.yaml"):
|
|
643
|
+
if not file_path.is_file():
|
|
644
|
+
continue
|
|
645
|
+
|
|
646
|
+
try:
|
|
647
|
+
mtime = os.path.getmtime(file_path) # noqa: PTH204
|
|
648
|
+
except OSError:
|
|
649
|
+
continue
|
|
650
|
+
|
|
651
|
+
str_path = str(file_path)
|
|
652
|
+
|
|
653
|
+
if str_path in self._file_mtimes and mtime > self._file_mtimes[str_path]:
|
|
654
|
+
try:
|
|
655
|
+
protocol = load_protocol(file_path)
|
|
656
|
+
reloaded.append(protocol)
|
|
657
|
+
|
|
658
|
+
# Auto-register if registry provided
|
|
659
|
+
if self.registry:
|
|
660
|
+
self.registry.register(protocol, overwrite=True)
|
|
661
|
+
self.registry._notify_change(protocol)
|
|
662
|
+
|
|
663
|
+
self._notify(protocol)
|
|
664
|
+
logger.info(f"Hot-reloaded protocol: {protocol.name} from {file_path}")
|
|
665
|
+
except Exception as e:
|
|
666
|
+
logger.warning(f"Failed to reload {file_path}: {e}")
|
|
667
|
+
|
|
668
|
+
self._file_mtimes[str_path] = mtime
|
|
669
|
+
|
|
670
|
+
return reloaded
|
|
671
|
+
|
|
672
|
+
def _scan_files(self) -> None:
|
|
673
|
+
"""Initial scan of directory."""
|
|
674
|
+
for file_path in self.directory.glob("**/*.yaml"):
|
|
675
|
+
if file_path.is_file():
|
|
676
|
+
with contextlib.suppress(OSError):
|
|
677
|
+
self._file_mtimes[str(file_path)] = os.path.getmtime(file_path) # noqa: PTH204
|
|
678
|
+
|
|
679
|
+
def _notify(self, protocol: ProtocolDefinition) -> None:
|
|
680
|
+
"""Notify callbacks of protocol change."""
|
|
681
|
+
for callback in self._callbacks:
|
|
682
|
+
try:
|
|
683
|
+
callback(protocol)
|
|
684
|
+
except Exception as e:
|
|
685
|
+
logger.warning(f"Protocol change callback failed: {e}")
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
# Global registry instance
|
|
689
|
+
_registry: ProtocolRegistry | None = None
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def get_protocol_registry() -> ProtocolRegistry:
|
|
693
|
+
"""Get the global protocol registry.
|
|
694
|
+
|
|
695
|
+
Returns:
|
|
696
|
+
Global ProtocolRegistry instance
|
|
697
|
+
"""
|
|
698
|
+
global _registry
|
|
699
|
+
if _registry is None:
|
|
700
|
+
_registry = ProtocolRegistry()
|
|
701
|
+
_register_builtin_protocols(_registry)
|
|
702
|
+
return _registry
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _register_builtin_protocols(registry: ProtocolRegistry) -> None:
|
|
706
|
+
"""Register built-in protocol definitions."""
|
|
707
|
+
# UART
|
|
708
|
+
registry.register(
|
|
709
|
+
ProtocolDefinition(
|
|
710
|
+
name="uart",
|
|
711
|
+
version="1.0.0",
|
|
712
|
+
description="Universal Asynchronous Receiver/Transmitter",
|
|
713
|
+
timing={
|
|
714
|
+
"baud_rates": [
|
|
715
|
+
9600,
|
|
716
|
+
19200,
|
|
717
|
+
38400,
|
|
718
|
+
57600,
|
|
719
|
+
115200,
|
|
720
|
+
230400,
|
|
721
|
+
460800,
|
|
722
|
+
921600,
|
|
723
|
+
],
|
|
724
|
+
"data_bits": [7, 8],
|
|
725
|
+
"stop_bits": [1, 1.5, 2],
|
|
726
|
+
"parity": ["none", "even", "odd", "mark", "space"],
|
|
727
|
+
},
|
|
728
|
+
voltage_levels={"logic_family": "TTL", "idle_state": "high"},
|
|
729
|
+
state_machine={
|
|
730
|
+
"states": ["IDLE", "START", "DATA", "PARITY", "STOP"],
|
|
731
|
+
"initial_state": "IDLE",
|
|
732
|
+
},
|
|
733
|
+
)
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
# SPI
|
|
737
|
+
registry.register(
|
|
738
|
+
ProtocolDefinition(
|
|
739
|
+
name="spi",
|
|
740
|
+
version="1.0.0",
|
|
741
|
+
description="Serial Peripheral Interface",
|
|
742
|
+
timing={
|
|
743
|
+
"data_bits": [8, 16, 32],
|
|
744
|
+
"clock_polarity": [0, 1],
|
|
745
|
+
"clock_phase": [0, 1],
|
|
746
|
+
},
|
|
747
|
+
state_machine={"states": ["IDLE", "ACTIVE"], "initial_state": "IDLE"},
|
|
748
|
+
)
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
# I2C
|
|
752
|
+
registry.register(
|
|
753
|
+
ProtocolDefinition(
|
|
754
|
+
name="i2c",
|
|
755
|
+
version="1.0.0",
|
|
756
|
+
description="Inter-Integrated Circuit",
|
|
757
|
+
timing={
|
|
758
|
+
"speed_modes": ["standard", "fast", "fast_plus", "high_speed"],
|
|
759
|
+
"data_bits": [8],
|
|
760
|
+
},
|
|
761
|
+
state_machine={
|
|
762
|
+
"states": ["IDLE", "START", "ADDRESS", "DATA", "ACK", "STOP"],
|
|
763
|
+
"initial_state": "IDLE",
|
|
764
|
+
},
|
|
765
|
+
)
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
# CAN
|
|
769
|
+
registry.register(
|
|
770
|
+
ProtocolDefinition(
|
|
771
|
+
name="can",
|
|
772
|
+
version="1.0.0",
|
|
773
|
+
description="Controller Area Network",
|
|
774
|
+
timing={"baud_rates": [125000, 250000, 500000, 1000000]},
|
|
775
|
+
state_machine={
|
|
776
|
+
"states": [
|
|
777
|
+
"IDLE",
|
|
778
|
+
"SOF",
|
|
779
|
+
"ARBITRATION",
|
|
780
|
+
"CONTROL",
|
|
781
|
+
"DATA",
|
|
782
|
+
"CRC",
|
|
783
|
+
"ACK",
|
|
784
|
+
"EOF",
|
|
785
|
+
],
|
|
786
|
+
"initial_state": "IDLE",
|
|
787
|
+
},
|
|
788
|
+
)
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def migrate_protocol_schema(
|
|
793
|
+
protocol_data: dict[str, Any], from_version: str, to_version: str = "1.0.0"
|
|
794
|
+
) -> dict[str, Any]:
|
|
795
|
+
"""Migrate protocol definition between schema versions.
|
|
796
|
+
|
|
797
|
+
Args:
|
|
798
|
+
protocol_data: Protocol data dictionary
|
|
799
|
+
from_version: Source schema version
|
|
800
|
+
to_version: Target schema version (default current)
|
|
801
|
+
|
|
802
|
+
Returns:
|
|
803
|
+
Migrated protocol data
|
|
804
|
+
|
|
805
|
+
Raises:
|
|
806
|
+
ConfigurationError: If migration fails or unsupported version
|
|
807
|
+
|
|
808
|
+
Example:
|
|
809
|
+
>>> old_proto = {"name": "uart", "timing": {...}}
|
|
810
|
+
>>> new_proto = migrate_protocol_schema(old_proto, "0.9.0", "1.0.0")
|
|
811
|
+
"""
|
|
812
|
+
if from_version == to_version:
|
|
813
|
+
return protocol_data
|
|
814
|
+
|
|
815
|
+
# Define migration paths
|
|
816
|
+
migrations = {
|
|
817
|
+
("0.9.0", "1.0.0"): _migrate_0_9_to_1_0,
|
|
818
|
+
("0.8.0", "0.9.0"): _migrate_0_8_to_0_9,
|
|
819
|
+
("0.8.0", "1.0.0"): lambda d: _migrate_0_9_to_1_0(_migrate_0_8_to_0_9(d)),
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
migration_key = (from_version, to_version)
|
|
823
|
+
if migration_key not in migrations:
|
|
824
|
+
raise ConfigurationError(
|
|
825
|
+
f"No migration path from schema {from_version} to {to_version}",
|
|
826
|
+
details="Supported migrations: " + ", ".join(f"{k[0]}→{k[1]}" for k in migrations),
|
|
827
|
+
fix_hint="Manually update the protocol definition or use an intermediate version",
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
logger.info(f"Migrating protocol schema from {from_version} to {to_version}")
|
|
831
|
+
try:
|
|
832
|
+
migrated = migrations[migration_key](protocol_data.copy()) # type: ignore[no-untyped-call]
|
|
833
|
+
migrated["schema_version"] = to_version
|
|
834
|
+
return migrated
|
|
835
|
+
except Exception as e:
|
|
836
|
+
raise ConfigurationError(
|
|
837
|
+
f"Schema migration failed from {from_version} to {to_version}",
|
|
838
|
+
details=str(e),
|
|
839
|
+
fix_hint="Check migration logs and manually update protocol definition",
|
|
840
|
+
) from e
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def _migrate_0_8_to_0_9(data: dict[str, Any]) -> dict[str, Any]:
|
|
844
|
+
"""Migrate from schema 0.8.0 to 0.9.0."""
|
|
845
|
+
# Example migration: rename 'baudrate' to 'baud_rates' and convert to list
|
|
846
|
+
if "baudrate" in data.get("timing", {}):
|
|
847
|
+
data.setdefault("timing", {})
|
|
848
|
+
data["timing"]["baud_rates"] = [data["timing"].pop("baudrate")]
|
|
849
|
+
return data
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def _migrate_0_9_to_1_0(data: dict[str, Any]) -> dict[str, Any]:
|
|
853
|
+
"""Migrate from schema 0.9.0 to 1.0.0."""
|
|
854
|
+
# Example migration: add required fields with defaults
|
|
855
|
+
data.setdefault("version", "1.0.0")
|
|
856
|
+
data.setdefault("description", "")
|
|
857
|
+
data.setdefault("author", "")
|
|
858
|
+
|
|
859
|
+
# Convert old state format if needed
|
|
860
|
+
if "state" in data:
|
|
861
|
+
data["state_machine"] = data.pop("state")
|
|
862
|
+
|
|
863
|
+
return data
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
__all__ = [
|
|
867
|
+
"ProtocolCapabilities",
|
|
868
|
+
"ProtocolDefinition",
|
|
869
|
+
"ProtocolRegistry",
|
|
870
|
+
"ProtocolWatcher",
|
|
871
|
+
"get_protocol_registry",
|
|
872
|
+
"load_protocol",
|
|
873
|
+
"migrate_protocol_schema",
|
|
874
|
+
"resolve_inheritance",
|
|
875
|
+
]
|