oscura 0.0.1__py3-none-any.whl → 0.1.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.
Files changed (465) hide show
  1. oscura/__init__.py +813 -8
  2. oscura/__main__.py +392 -0
  3. oscura/analyzers/__init__.py +37 -0
  4. oscura/analyzers/digital/__init__.py +177 -0
  5. oscura/analyzers/digital/bus.py +691 -0
  6. oscura/analyzers/digital/clock.py +805 -0
  7. oscura/analyzers/digital/correlation.py +720 -0
  8. oscura/analyzers/digital/edges.py +632 -0
  9. oscura/analyzers/digital/extraction.py +413 -0
  10. oscura/analyzers/digital/quality.py +878 -0
  11. oscura/analyzers/digital/signal_quality.py +877 -0
  12. oscura/analyzers/digital/thresholds.py +708 -0
  13. oscura/analyzers/digital/timing.py +1104 -0
  14. oscura/analyzers/eye/__init__.py +46 -0
  15. oscura/analyzers/eye/diagram.py +434 -0
  16. oscura/analyzers/eye/metrics.py +555 -0
  17. oscura/analyzers/jitter/__init__.py +83 -0
  18. oscura/analyzers/jitter/ber.py +333 -0
  19. oscura/analyzers/jitter/decomposition.py +759 -0
  20. oscura/analyzers/jitter/measurements.py +413 -0
  21. oscura/analyzers/jitter/spectrum.py +220 -0
  22. oscura/analyzers/measurements.py +40 -0
  23. oscura/analyzers/packet/__init__.py +171 -0
  24. oscura/analyzers/packet/daq.py +1077 -0
  25. oscura/analyzers/packet/metrics.py +437 -0
  26. oscura/analyzers/packet/parser.py +327 -0
  27. oscura/analyzers/packet/payload.py +2156 -0
  28. oscura/analyzers/packet/payload_analysis.py +1312 -0
  29. oscura/analyzers/packet/payload_extraction.py +236 -0
  30. oscura/analyzers/packet/payload_patterns.py +670 -0
  31. oscura/analyzers/packet/stream.py +359 -0
  32. oscura/analyzers/patterns/__init__.py +266 -0
  33. oscura/analyzers/patterns/clustering.py +1036 -0
  34. oscura/analyzers/patterns/discovery.py +539 -0
  35. oscura/analyzers/patterns/learning.py +797 -0
  36. oscura/analyzers/patterns/matching.py +1091 -0
  37. oscura/analyzers/patterns/periodic.py +650 -0
  38. oscura/analyzers/patterns/sequences.py +767 -0
  39. oscura/analyzers/power/__init__.py +116 -0
  40. oscura/analyzers/power/ac_power.py +391 -0
  41. oscura/analyzers/power/basic.py +383 -0
  42. oscura/analyzers/power/conduction.py +314 -0
  43. oscura/analyzers/power/efficiency.py +297 -0
  44. oscura/analyzers/power/ripple.py +356 -0
  45. oscura/analyzers/power/soa.py +372 -0
  46. oscura/analyzers/power/switching.py +479 -0
  47. oscura/analyzers/protocol/__init__.py +150 -0
  48. oscura/analyzers/protocols/__init__.py +150 -0
  49. oscura/analyzers/protocols/base.py +500 -0
  50. oscura/analyzers/protocols/can.py +620 -0
  51. oscura/analyzers/protocols/can_fd.py +448 -0
  52. oscura/analyzers/protocols/flexray.py +405 -0
  53. oscura/analyzers/protocols/hdlc.py +399 -0
  54. oscura/analyzers/protocols/i2c.py +368 -0
  55. oscura/analyzers/protocols/i2s.py +296 -0
  56. oscura/analyzers/protocols/jtag.py +393 -0
  57. oscura/analyzers/protocols/lin.py +445 -0
  58. oscura/analyzers/protocols/manchester.py +333 -0
  59. oscura/analyzers/protocols/onewire.py +501 -0
  60. oscura/analyzers/protocols/spi.py +334 -0
  61. oscura/analyzers/protocols/swd.py +325 -0
  62. oscura/analyzers/protocols/uart.py +393 -0
  63. oscura/analyzers/protocols/usb.py +495 -0
  64. oscura/analyzers/signal_integrity/__init__.py +63 -0
  65. oscura/analyzers/signal_integrity/embedding.py +294 -0
  66. oscura/analyzers/signal_integrity/equalization.py +370 -0
  67. oscura/analyzers/signal_integrity/sparams.py +484 -0
  68. oscura/analyzers/spectral/__init__.py +53 -0
  69. oscura/analyzers/spectral/chunked.py +273 -0
  70. oscura/analyzers/spectral/chunked_fft.py +571 -0
  71. oscura/analyzers/spectral/chunked_wavelet.py +391 -0
  72. oscura/analyzers/spectral/fft.py +92 -0
  73. oscura/analyzers/statistical/__init__.py +250 -0
  74. oscura/analyzers/statistical/checksum.py +923 -0
  75. oscura/analyzers/statistical/chunked_corr.py +228 -0
  76. oscura/analyzers/statistical/classification.py +778 -0
  77. oscura/analyzers/statistical/entropy.py +1113 -0
  78. oscura/analyzers/statistical/ngrams.py +614 -0
  79. oscura/analyzers/statistics/__init__.py +119 -0
  80. oscura/analyzers/statistics/advanced.py +885 -0
  81. oscura/analyzers/statistics/basic.py +263 -0
  82. oscura/analyzers/statistics/correlation.py +630 -0
  83. oscura/analyzers/statistics/distribution.py +298 -0
  84. oscura/analyzers/statistics/outliers.py +463 -0
  85. oscura/analyzers/statistics/streaming.py +93 -0
  86. oscura/analyzers/statistics/trend.py +520 -0
  87. oscura/analyzers/validation.py +598 -0
  88. oscura/analyzers/waveform/__init__.py +36 -0
  89. oscura/analyzers/waveform/measurements.py +943 -0
  90. oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
  91. oscura/analyzers/waveform/spectral.py +1689 -0
  92. oscura/analyzers/waveform/wavelets.py +298 -0
  93. oscura/api/__init__.py +62 -0
  94. oscura/api/dsl.py +538 -0
  95. oscura/api/fluent.py +571 -0
  96. oscura/api/operators.py +498 -0
  97. oscura/api/optimization.py +392 -0
  98. oscura/api/profiling.py +396 -0
  99. oscura/automotive/__init__.py +73 -0
  100. oscura/automotive/can/__init__.py +52 -0
  101. oscura/automotive/can/analysis.py +356 -0
  102. oscura/automotive/can/checksum.py +250 -0
  103. oscura/automotive/can/correlation.py +212 -0
  104. oscura/automotive/can/discovery.py +355 -0
  105. oscura/automotive/can/message_wrapper.py +375 -0
  106. oscura/automotive/can/models.py +385 -0
  107. oscura/automotive/can/patterns.py +381 -0
  108. oscura/automotive/can/session.py +452 -0
  109. oscura/automotive/can/state_machine.py +300 -0
  110. oscura/automotive/can/stimulus_response.py +461 -0
  111. oscura/automotive/dbc/__init__.py +15 -0
  112. oscura/automotive/dbc/generator.py +156 -0
  113. oscura/automotive/dbc/parser.py +146 -0
  114. oscura/automotive/dtc/__init__.py +30 -0
  115. oscura/automotive/dtc/database.py +3036 -0
  116. oscura/automotive/j1939/__init__.py +14 -0
  117. oscura/automotive/j1939/decoder.py +745 -0
  118. oscura/automotive/loaders/__init__.py +35 -0
  119. oscura/automotive/loaders/asc.py +98 -0
  120. oscura/automotive/loaders/blf.py +77 -0
  121. oscura/automotive/loaders/csv_can.py +136 -0
  122. oscura/automotive/loaders/dispatcher.py +136 -0
  123. oscura/automotive/loaders/mdf.py +331 -0
  124. oscura/automotive/loaders/pcap.py +132 -0
  125. oscura/automotive/obd/__init__.py +14 -0
  126. oscura/automotive/obd/decoder.py +707 -0
  127. oscura/automotive/uds/__init__.py +48 -0
  128. oscura/automotive/uds/decoder.py +265 -0
  129. oscura/automotive/uds/models.py +64 -0
  130. oscura/automotive/visualization.py +369 -0
  131. oscura/batch/__init__.py +55 -0
  132. oscura/batch/advanced.py +627 -0
  133. oscura/batch/aggregate.py +300 -0
  134. oscura/batch/analyze.py +139 -0
  135. oscura/batch/logging.py +487 -0
  136. oscura/batch/metrics.py +556 -0
  137. oscura/builders/__init__.py +41 -0
  138. oscura/builders/signal_builder.py +1131 -0
  139. oscura/cli/__init__.py +14 -0
  140. oscura/cli/batch.py +339 -0
  141. oscura/cli/characterize.py +273 -0
  142. oscura/cli/compare.py +775 -0
  143. oscura/cli/decode.py +551 -0
  144. oscura/cli/main.py +247 -0
  145. oscura/cli/shell.py +350 -0
  146. oscura/comparison/__init__.py +66 -0
  147. oscura/comparison/compare.py +397 -0
  148. oscura/comparison/golden.py +487 -0
  149. oscura/comparison/limits.py +391 -0
  150. oscura/comparison/mask.py +434 -0
  151. oscura/comparison/trace_diff.py +30 -0
  152. oscura/comparison/visualization.py +481 -0
  153. oscura/compliance/__init__.py +70 -0
  154. oscura/compliance/advanced.py +756 -0
  155. oscura/compliance/masks.py +363 -0
  156. oscura/compliance/reporting.py +483 -0
  157. oscura/compliance/testing.py +298 -0
  158. oscura/component/__init__.py +38 -0
  159. oscura/component/impedance.py +365 -0
  160. oscura/component/reactive.py +598 -0
  161. oscura/component/transmission_line.py +312 -0
  162. oscura/config/__init__.py +191 -0
  163. oscura/config/defaults.py +254 -0
  164. oscura/config/loader.py +348 -0
  165. oscura/config/memory.py +271 -0
  166. oscura/config/migration.py +458 -0
  167. oscura/config/pipeline.py +1077 -0
  168. oscura/config/preferences.py +530 -0
  169. oscura/config/protocol.py +875 -0
  170. oscura/config/schema.py +713 -0
  171. oscura/config/settings.py +420 -0
  172. oscura/config/thresholds.py +599 -0
  173. oscura/convenience.py +457 -0
  174. oscura/core/__init__.py +299 -0
  175. oscura/core/audit.py +457 -0
  176. oscura/core/backend_selector.py +405 -0
  177. oscura/core/cache.py +590 -0
  178. oscura/core/cancellation.py +439 -0
  179. oscura/core/confidence.py +225 -0
  180. oscura/core/config.py +506 -0
  181. oscura/core/correlation.py +216 -0
  182. oscura/core/cross_domain.py +422 -0
  183. oscura/core/debug.py +301 -0
  184. oscura/core/edge_cases.py +541 -0
  185. oscura/core/exceptions.py +535 -0
  186. oscura/core/gpu_backend.py +523 -0
  187. oscura/core/lazy.py +832 -0
  188. oscura/core/log_query.py +540 -0
  189. oscura/core/logging.py +931 -0
  190. oscura/core/logging_advanced.py +952 -0
  191. oscura/core/memoize.py +171 -0
  192. oscura/core/memory_check.py +274 -0
  193. oscura/core/memory_guard.py +290 -0
  194. oscura/core/memory_limits.py +336 -0
  195. oscura/core/memory_monitor.py +453 -0
  196. oscura/core/memory_progress.py +465 -0
  197. oscura/core/memory_warnings.py +315 -0
  198. oscura/core/numba_backend.py +362 -0
  199. oscura/core/performance.py +352 -0
  200. oscura/core/progress.py +524 -0
  201. oscura/core/provenance.py +358 -0
  202. oscura/core/results.py +331 -0
  203. oscura/core/types.py +504 -0
  204. oscura/core/uncertainty.py +383 -0
  205. oscura/discovery/__init__.py +52 -0
  206. oscura/discovery/anomaly_detector.py +672 -0
  207. oscura/discovery/auto_decoder.py +415 -0
  208. oscura/discovery/comparison.py +497 -0
  209. oscura/discovery/quality_validator.py +528 -0
  210. oscura/discovery/signal_detector.py +769 -0
  211. oscura/dsl/__init__.py +73 -0
  212. oscura/dsl/commands.py +246 -0
  213. oscura/dsl/interpreter.py +455 -0
  214. oscura/dsl/parser.py +689 -0
  215. oscura/dsl/repl.py +172 -0
  216. oscura/exceptions.py +59 -0
  217. oscura/exploratory/__init__.py +111 -0
  218. oscura/exploratory/error_recovery.py +642 -0
  219. oscura/exploratory/fuzzy.py +513 -0
  220. oscura/exploratory/fuzzy_advanced.py +786 -0
  221. oscura/exploratory/legacy.py +831 -0
  222. oscura/exploratory/parse.py +358 -0
  223. oscura/exploratory/recovery.py +275 -0
  224. oscura/exploratory/sync.py +382 -0
  225. oscura/exploratory/unknown.py +707 -0
  226. oscura/export/__init__.py +25 -0
  227. oscura/export/wireshark/README.md +265 -0
  228. oscura/export/wireshark/__init__.py +47 -0
  229. oscura/export/wireshark/generator.py +312 -0
  230. oscura/export/wireshark/lua_builder.py +159 -0
  231. oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
  232. oscura/export/wireshark/type_mapping.py +165 -0
  233. oscura/export/wireshark/validator.py +105 -0
  234. oscura/exporters/__init__.py +94 -0
  235. oscura/exporters/csv.py +303 -0
  236. oscura/exporters/exporters.py +44 -0
  237. oscura/exporters/hdf5.py +219 -0
  238. oscura/exporters/html_export.py +701 -0
  239. oscura/exporters/json_export.py +291 -0
  240. oscura/exporters/markdown_export.py +367 -0
  241. oscura/exporters/matlab_export.py +354 -0
  242. oscura/exporters/npz_export.py +219 -0
  243. oscura/exporters/spice_export.py +210 -0
  244. oscura/extensibility/__init__.py +131 -0
  245. oscura/extensibility/docs.py +752 -0
  246. oscura/extensibility/extensions.py +1125 -0
  247. oscura/extensibility/logging.py +259 -0
  248. oscura/extensibility/measurements.py +485 -0
  249. oscura/extensibility/plugins.py +414 -0
  250. oscura/extensibility/registry.py +346 -0
  251. oscura/extensibility/templates.py +913 -0
  252. oscura/extensibility/validation.py +651 -0
  253. oscura/filtering/__init__.py +89 -0
  254. oscura/filtering/base.py +563 -0
  255. oscura/filtering/convenience.py +564 -0
  256. oscura/filtering/design.py +725 -0
  257. oscura/filtering/filters.py +32 -0
  258. oscura/filtering/introspection.py +605 -0
  259. oscura/guidance/__init__.py +24 -0
  260. oscura/guidance/recommender.py +429 -0
  261. oscura/guidance/wizard.py +518 -0
  262. oscura/inference/__init__.py +251 -0
  263. oscura/inference/active_learning/README.md +153 -0
  264. oscura/inference/active_learning/__init__.py +38 -0
  265. oscura/inference/active_learning/lstar.py +257 -0
  266. oscura/inference/active_learning/observation_table.py +230 -0
  267. oscura/inference/active_learning/oracle.py +78 -0
  268. oscura/inference/active_learning/teachers/__init__.py +15 -0
  269. oscura/inference/active_learning/teachers/simulator.py +192 -0
  270. oscura/inference/adaptive_tuning.py +453 -0
  271. oscura/inference/alignment.py +653 -0
  272. oscura/inference/bayesian.py +943 -0
  273. oscura/inference/binary.py +1016 -0
  274. oscura/inference/crc_reverse.py +711 -0
  275. oscura/inference/logic.py +288 -0
  276. oscura/inference/message_format.py +1305 -0
  277. oscura/inference/protocol.py +417 -0
  278. oscura/inference/protocol_dsl.py +1084 -0
  279. oscura/inference/protocol_library.py +1230 -0
  280. oscura/inference/sequences.py +809 -0
  281. oscura/inference/signal_intelligence.py +1509 -0
  282. oscura/inference/spectral.py +215 -0
  283. oscura/inference/state_machine.py +634 -0
  284. oscura/inference/stream.py +918 -0
  285. oscura/integrations/__init__.py +59 -0
  286. oscura/integrations/llm.py +1827 -0
  287. oscura/jupyter/__init__.py +32 -0
  288. oscura/jupyter/display.py +268 -0
  289. oscura/jupyter/magic.py +334 -0
  290. oscura/loaders/__init__.py +526 -0
  291. oscura/loaders/binary.py +69 -0
  292. oscura/loaders/configurable.py +1255 -0
  293. oscura/loaders/csv.py +26 -0
  294. oscura/loaders/csv_loader.py +473 -0
  295. oscura/loaders/hdf5.py +9 -0
  296. oscura/loaders/hdf5_loader.py +510 -0
  297. oscura/loaders/lazy.py +370 -0
  298. oscura/loaders/mmap_loader.py +583 -0
  299. oscura/loaders/numpy_loader.py +436 -0
  300. oscura/loaders/pcap.py +432 -0
  301. oscura/loaders/preprocessing.py +368 -0
  302. oscura/loaders/rigol.py +287 -0
  303. oscura/loaders/sigrok.py +321 -0
  304. oscura/loaders/tdms.py +367 -0
  305. oscura/loaders/tektronix.py +711 -0
  306. oscura/loaders/validation.py +584 -0
  307. oscura/loaders/vcd.py +464 -0
  308. oscura/loaders/wav.py +233 -0
  309. oscura/math/__init__.py +45 -0
  310. oscura/math/arithmetic.py +824 -0
  311. oscura/math/interpolation.py +413 -0
  312. oscura/onboarding/__init__.py +39 -0
  313. oscura/onboarding/help.py +498 -0
  314. oscura/onboarding/tutorials.py +405 -0
  315. oscura/onboarding/wizard.py +466 -0
  316. oscura/optimization/__init__.py +19 -0
  317. oscura/optimization/parallel.py +440 -0
  318. oscura/optimization/search.py +532 -0
  319. oscura/pipeline/__init__.py +43 -0
  320. oscura/pipeline/base.py +338 -0
  321. oscura/pipeline/composition.py +242 -0
  322. oscura/pipeline/parallel.py +448 -0
  323. oscura/pipeline/pipeline.py +375 -0
  324. oscura/pipeline/reverse_engineering.py +1119 -0
  325. oscura/plugins/__init__.py +122 -0
  326. oscura/plugins/base.py +272 -0
  327. oscura/plugins/cli.py +497 -0
  328. oscura/plugins/discovery.py +411 -0
  329. oscura/plugins/isolation.py +418 -0
  330. oscura/plugins/lifecycle.py +959 -0
  331. oscura/plugins/manager.py +493 -0
  332. oscura/plugins/registry.py +421 -0
  333. oscura/plugins/versioning.py +372 -0
  334. oscura/py.typed +0 -0
  335. oscura/quality/__init__.py +65 -0
  336. oscura/quality/ensemble.py +740 -0
  337. oscura/quality/explainer.py +338 -0
  338. oscura/quality/scoring.py +616 -0
  339. oscura/quality/warnings.py +456 -0
  340. oscura/reporting/__init__.py +248 -0
  341. oscura/reporting/advanced.py +1234 -0
  342. oscura/reporting/analyze.py +448 -0
  343. oscura/reporting/argument_preparer.py +596 -0
  344. oscura/reporting/auto_report.py +507 -0
  345. oscura/reporting/batch.py +615 -0
  346. oscura/reporting/chart_selection.py +223 -0
  347. oscura/reporting/comparison.py +330 -0
  348. oscura/reporting/config.py +615 -0
  349. oscura/reporting/content/__init__.py +39 -0
  350. oscura/reporting/content/executive.py +127 -0
  351. oscura/reporting/content/filtering.py +191 -0
  352. oscura/reporting/content/minimal.py +257 -0
  353. oscura/reporting/content/verbosity.py +162 -0
  354. oscura/reporting/core.py +508 -0
  355. oscura/reporting/core_formats/__init__.py +17 -0
  356. oscura/reporting/core_formats/multi_format.py +210 -0
  357. oscura/reporting/engine.py +836 -0
  358. oscura/reporting/export.py +366 -0
  359. oscura/reporting/formatting/__init__.py +129 -0
  360. oscura/reporting/formatting/emphasis.py +81 -0
  361. oscura/reporting/formatting/numbers.py +403 -0
  362. oscura/reporting/formatting/standards.py +55 -0
  363. oscura/reporting/formatting.py +466 -0
  364. oscura/reporting/html.py +578 -0
  365. oscura/reporting/index.py +590 -0
  366. oscura/reporting/multichannel.py +296 -0
  367. oscura/reporting/output.py +379 -0
  368. oscura/reporting/pdf.py +373 -0
  369. oscura/reporting/plots.py +731 -0
  370. oscura/reporting/pptx_export.py +360 -0
  371. oscura/reporting/renderers/__init__.py +11 -0
  372. oscura/reporting/renderers/pdf.py +94 -0
  373. oscura/reporting/sections.py +471 -0
  374. oscura/reporting/standards.py +680 -0
  375. oscura/reporting/summary_generator.py +368 -0
  376. oscura/reporting/tables.py +397 -0
  377. oscura/reporting/template_system.py +724 -0
  378. oscura/reporting/templates/__init__.py +15 -0
  379. oscura/reporting/templates/definition.py +205 -0
  380. oscura/reporting/templates/index.html +649 -0
  381. oscura/reporting/templates/index.md +173 -0
  382. oscura/schemas/__init__.py +158 -0
  383. oscura/schemas/bus_configuration.json +322 -0
  384. oscura/schemas/device_mapping.json +182 -0
  385. oscura/schemas/packet_format.json +418 -0
  386. oscura/schemas/protocol_definition.json +363 -0
  387. oscura/search/__init__.py +16 -0
  388. oscura/search/anomaly.py +292 -0
  389. oscura/search/context.py +149 -0
  390. oscura/search/pattern.py +160 -0
  391. oscura/session/__init__.py +34 -0
  392. oscura/session/annotations.py +289 -0
  393. oscura/session/history.py +313 -0
  394. oscura/session/session.py +445 -0
  395. oscura/streaming/__init__.py +43 -0
  396. oscura/streaming/chunked.py +611 -0
  397. oscura/streaming/progressive.py +393 -0
  398. oscura/streaming/realtime.py +622 -0
  399. oscura/testing/__init__.py +54 -0
  400. oscura/testing/synthetic.py +808 -0
  401. oscura/triggering/__init__.py +68 -0
  402. oscura/triggering/base.py +229 -0
  403. oscura/triggering/edge.py +353 -0
  404. oscura/triggering/pattern.py +344 -0
  405. oscura/triggering/pulse.py +581 -0
  406. oscura/triggering/window.py +453 -0
  407. oscura/ui/__init__.py +48 -0
  408. oscura/ui/formatters.py +526 -0
  409. oscura/ui/progressive_display.py +340 -0
  410. oscura/utils/__init__.py +99 -0
  411. oscura/utils/autodetect.py +338 -0
  412. oscura/utils/buffer.py +389 -0
  413. oscura/utils/lazy.py +407 -0
  414. oscura/utils/lazy_imports.py +147 -0
  415. oscura/utils/memory.py +836 -0
  416. oscura/utils/memory_advanced.py +1326 -0
  417. oscura/utils/memory_extensions.py +465 -0
  418. oscura/utils/progressive.py +352 -0
  419. oscura/utils/windowing.py +362 -0
  420. oscura/visualization/__init__.py +321 -0
  421. oscura/visualization/accessibility.py +526 -0
  422. oscura/visualization/annotations.py +374 -0
  423. oscura/visualization/axis_scaling.py +305 -0
  424. oscura/visualization/colors.py +453 -0
  425. oscura/visualization/digital.py +337 -0
  426. oscura/visualization/eye.py +420 -0
  427. oscura/visualization/histogram.py +281 -0
  428. oscura/visualization/interactive.py +858 -0
  429. oscura/visualization/jitter.py +702 -0
  430. oscura/visualization/keyboard.py +394 -0
  431. oscura/visualization/layout.py +365 -0
  432. oscura/visualization/optimization.py +1028 -0
  433. oscura/visualization/palettes.py +446 -0
  434. oscura/visualization/plot.py +92 -0
  435. oscura/visualization/power.py +290 -0
  436. oscura/visualization/power_extended.py +626 -0
  437. oscura/visualization/presets.py +467 -0
  438. oscura/visualization/protocols.py +932 -0
  439. oscura/visualization/render.py +207 -0
  440. oscura/visualization/rendering.py +444 -0
  441. oscura/visualization/reverse_engineering.py +791 -0
  442. oscura/visualization/signal_integrity.py +808 -0
  443. oscura/visualization/specialized.py +553 -0
  444. oscura/visualization/spectral.py +811 -0
  445. oscura/visualization/styles.py +381 -0
  446. oscura/visualization/thumbnails.py +311 -0
  447. oscura/visualization/time_axis.py +351 -0
  448. oscura/visualization/waveform.py +367 -0
  449. oscura/workflow/__init__.py +13 -0
  450. oscura/workflow/dag.py +377 -0
  451. oscura/workflows/__init__.py +58 -0
  452. oscura/workflows/compliance.py +280 -0
  453. oscura/workflows/digital.py +272 -0
  454. oscura/workflows/multi_trace.py +502 -0
  455. oscura/workflows/power.py +178 -0
  456. oscura/workflows/protocol.py +492 -0
  457. oscura/workflows/reverse_engineering.py +639 -0
  458. oscura/workflows/signal_integrity.py +227 -0
  459. oscura-0.1.0.dist-info/METADATA +300 -0
  460. oscura-0.1.0.dist-info/RECORD +463 -0
  461. oscura-0.1.0.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
  463. oscura-0.0.1.dist-info/METADATA +0 -63
  464. oscura-0.0.1.dist-info/RECORD +0 -5
  465. {oscura-0.0.1.dist-info → oscura-0.1.0.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
+ ]