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.
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.1.dist-info/METADATA +300 -0
  460. oscura-0.1.1.dist-info/RECORD +463 -0
  461. oscura-0.1.1.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.1.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.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,932 @@
1
+ """Protocol decoder visualization functions.
2
+
3
+ This module provides visualization functions for decoded protocol packets,
4
+ creating timing diagrams with multi-level annotations for protocol analysis.
5
+
6
+ Example:
7
+ >>> from oscura.analyzers.protocols.uart import UARTDecoder
8
+ >>> from oscura.visualization.protocols import plot_protocol_decode
9
+ >>>
10
+ >>> decoder = UARTDecoder(baudrate=115200)
11
+ >>> packets = list(decoder.decode(trace))
12
+ >>> fig = plot_protocol_decode(packets, trace=trace, title="UART Decode")
13
+
14
+ References:
15
+ - Protocol visualization best practices
16
+ - Wavedrom-style timing diagrams
17
+ - sigrok annotation system
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import TYPE_CHECKING, Any, Literal
23
+
24
+ import numpy as np
25
+
26
+ if TYPE_CHECKING:
27
+ from matplotlib.axes import Axes
28
+ from matplotlib.figure import Figure
29
+ from numpy.typing import NDArray
30
+
31
+ from oscura.core.types import DigitalTrace, ProtocolPacket
32
+
33
+ try:
34
+ import matplotlib.pyplot as plt
35
+ from matplotlib import patches
36
+
37
+ HAS_MATPLOTLIB = True
38
+ except ImportError:
39
+ HAS_MATPLOTLIB = False
40
+
41
+
42
+ def plot_protocol_decode(
43
+ packets: list[ProtocolPacket],
44
+ *,
45
+ trace: DigitalTrace | None = None,
46
+ trace_channel: str | None = None,
47
+ annotation_levels: list[str] | Literal["all"] = "all",
48
+ time_range: tuple[float, float] | None = None,
49
+ time_unit: str = "auto",
50
+ show_data: bool = True,
51
+ show_errors: bool = True,
52
+ colorize: bool = True,
53
+ figsize: tuple[float, float] | None = None,
54
+ title: str | None = None,
55
+ ) -> Figure:
56
+ """Plot decoded protocol packets with multi-level annotations.
57
+
58
+ Creates a timing diagram showing the original waveform (if provided)
59
+ and annotation rows for decoded protocol data at different levels
60
+ (bits, bytes, fields, packets, messages).
61
+
62
+ Args:
63
+ packets: List of decoded protocol packets to visualize.
64
+ trace: Optional digital trace to plot alongside annotations.
65
+ trace_channel: Name of trace channel (default: protocol name).
66
+ annotation_levels: Which annotation levels to display ("all" or list of level names).
67
+ time_range: Time range to plot (t_min, t_max) in seconds. None = auto from packets.
68
+ time_unit: Time unit for x-axis ("s", "ms", "us", "ns", "auto").
69
+ show_data: Show decoded data values in annotations.
70
+ show_errors: Highlight packets with errors.
71
+ colorize: Use color coding for different packet types.
72
+ figsize: Figure size (width, height). Auto-calculated if None.
73
+ title: Plot title.
74
+
75
+ Returns:
76
+ Matplotlib Figure object.
77
+
78
+ Raises:
79
+ ImportError: If matplotlib is not available.
80
+ ValueError: If packets list is empty.
81
+
82
+ Example:
83
+ >>> decoder = UARTDecoder(baudrate=9600)
84
+ >>> packets = list(decoder.decode(rx_trace))
85
+ >>> fig = plot_protocol_decode(
86
+ ... packets,
87
+ ... trace=rx_trace,
88
+ ... time_unit="ms",
89
+ ... title="UART Communication"
90
+ ... )
91
+
92
+ References:
93
+ VIS-030: Protocol Decode Visualization
94
+ """
95
+ if not HAS_MATPLOTLIB:
96
+ raise ImportError("matplotlib is required for visualization")
97
+
98
+ if len(packets) == 0:
99
+ raise ValueError("packets list cannot be empty")
100
+
101
+ # Determine protocol name from first packet
102
+ protocol = packets[0].protocol
103
+
104
+ # Determine time range
105
+ if time_range is None:
106
+ t_min = min(p.timestamp for p in packets)
107
+ t_max = max(p.end_timestamp if p.end_timestamp else p.timestamp for p in packets)
108
+ # Add 10% padding
109
+ padding = (t_max - t_min) * 0.1
110
+ t_min -= padding
111
+ t_max += padding
112
+ else:
113
+ t_min, t_max = time_range
114
+
115
+ # Select time unit
116
+ if time_unit == "auto":
117
+ time_range_val = t_max - t_min
118
+ if time_range_val < 1e-6:
119
+ time_unit = "ns"
120
+ time_mult = 1e9
121
+ elif time_range_val < 1e-3:
122
+ time_unit = "us"
123
+ time_mult = 1e6
124
+ elif time_range_val < 1:
125
+ time_unit = "ms"
126
+ time_mult = 1e3
127
+ else:
128
+ time_unit = "s"
129
+ time_mult = 1.0
130
+ else:
131
+ time_mult = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
132
+
133
+ # Determine number of rows
134
+ n_rows = 1 if trace is None else 2 # Waveform + packets
135
+
136
+ # Auto-calculate figure size
137
+ if figsize is None:
138
+ width = 14
139
+ height = max(4, n_rows * 1.5 + 1)
140
+ figsize = (width, height)
141
+
142
+ fig, axes = plt.subplots(
143
+ n_rows,
144
+ 1,
145
+ figsize=figsize,
146
+ sharex=True,
147
+ gridspec_kw={"hspace": 0.15, "height_ratios": [1] * n_rows},
148
+ )
149
+
150
+ if n_rows == 1:
151
+ axes = [axes]
152
+
153
+ ax_idx = 0
154
+
155
+ # Plot waveform if provided
156
+ if trace is not None:
157
+ ax = axes[ax_idx]
158
+ ax_idx += 1
159
+
160
+ # Create time vector for trace
161
+ trace_time = trace.time_vector * time_mult
162
+ trace_data = trace.data.astype(float)
163
+
164
+ # Filter to time range
165
+ mask = (trace_time >= t_min * time_mult) & (trace_time <= t_max * time_mult)
166
+ trace_time = trace_time[mask]
167
+ trace_data = trace_data[mask]
168
+
169
+ # Plot as digital waveform
170
+ _plot_digital_waveform(ax, trace_time, trace_data)
171
+
172
+ channel_name = trace_channel if trace_channel else protocol
173
+ ax.set_ylabel(channel_name, rotation=0, ha="right", va="center", fontsize=10)
174
+ ax.set_ylim(-0.2, 1.3)
175
+ ax.set_yticks([])
176
+ ax.grid(True, axis="x", alpha=0.3, linestyle=":")
177
+
178
+ # Plot packets row
179
+ ax = axes[ax_idx]
180
+
181
+ # Plot packet timeline
182
+ for packet in packets:
183
+ if packet.timestamp < t_min or packet.timestamp > t_max:
184
+ continue
185
+
186
+ start = packet.timestamp * time_mult
187
+ end = (
188
+ packet.end_timestamp if packet.end_timestamp else packet.timestamp + 0.001
189
+ ) * time_mult
190
+
191
+ # Determine packet color
192
+ if show_errors and packet.errors:
193
+ color = "#ff6b6b" # Red for errors
194
+ elif colorize:
195
+ color = _get_packet_color(packet, protocol)
196
+ else:
197
+ color = "#4ecdc4" # Default teal
198
+
199
+ # Draw packet rectangle
200
+ rect = patches.Rectangle(
201
+ (start, 0.1),
202
+ end - start,
203
+ 0.8,
204
+ facecolor=color,
205
+ edgecolor="black",
206
+ linewidth=0.8,
207
+ alpha=0.7,
208
+ )
209
+ ax.add_patch(rect)
210
+
211
+ # Add data annotation
212
+ if show_data and packet.data:
213
+ data_str = _format_packet_data(packet)
214
+ mid_time = (start + end) / 2
215
+ ax.text(
216
+ mid_time,
217
+ 0.5,
218
+ data_str,
219
+ ha="center",
220
+ va="center",
221
+ fontsize=8,
222
+ fontweight="bold",
223
+ color="white" if not (show_errors and packet.errors) else "black",
224
+ )
225
+
226
+ # Add error markers
227
+ if show_errors and packet.errors:
228
+ ax.plot(
229
+ start,
230
+ 1.1,
231
+ "rx",
232
+ markersize=8,
233
+ markeredgewidth=2,
234
+ )
235
+
236
+ ax.set_ylabel(f"{protocol}\nPackets", rotation=0, ha="right", va="center", fontsize=10)
237
+ ax.set_ylim(0, 1.2)
238
+ ax.set_yticks([])
239
+ ax.grid(True, axis="x", alpha=0.3, linestyle=":")
240
+
241
+ # Set x-axis label
242
+ axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
243
+ axes[-1].set_xlim(t_min * time_mult, t_max * time_mult)
244
+
245
+ if title:
246
+ fig.suptitle(title, fontsize=14, y=0.98)
247
+
248
+ fig.tight_layout()
249
+ return fig
250
+
251
+
252
+ def plot_uart_decode(
253
+ packets: list[ProtocolPacket],
254
+ *,
255
+ rx_trace: DigitalTrace | None = None,
256
+ tx_trace: DigitalTrace | None = None,
257
+ time_range: tuple[float, float] | None = None,
258
+ time_unit: str = "auto",
259
+ show_parity_errors: bool = True,
260
+ show_framing_errors: bool = True,
261
+ figsize: tuple[float, float] | None = None,
262
+ title: str = "UART Communication",
263
+ ) -> Figure:
264
+ """Plot UART decoded packets with RX/TX lanes.
265
+
266
+ Specialized visualization for UART showing separate RX and TX channels
267
+ with decoded bytes and error highlighting.
268
+
269
+ Args:
270
+ packets: List of UART packets.
271
+ rx_trace: Optional RX digital trace.
272
+ tx_trace: Optional TX digital trace.
273
+ time_range: Time range to plot (t_min, t_max) in seconds.
274
+ time_unit: Time unit for x-axis ("s", "ms", "us", "ns", "auto").
275
+ show_parity_errors: Highlight parity errors.
276
+ show_framing_errors: Highlight framing errors.
277
+ figsize: Figure size (width, height).
278
+ title: Plot title.
279
+
280
+ Returns:
281
+ Matplotlib Figure object.
282
+
283
+ Raises:
284
+ ImportError: If matplotlib is not installed.
285
+ ValueError: If packets list is empty.
286
+
287
+ Example:
288
+ >>> decoder = UARTDecoder(baudrate=115200, parity="even")
289
+ >>> packets = list(decoder.decode(rx_trace))
290
+ >>> fig = plot_uart_decode(packets, rx_trace=rx_trace, time_unit="ms")
291
+ """
292
+ if not HAS_MATPLOTLIB:
293
+ raise ImportError("matplotlib is required for visualization")
294
+
295
+ if len(packets) == 0:
296
+ raise ValueError("packets list cannot be empty")
297
+
298
+ # If we have both RX and TX, create dual-channel visualization
299
+ if rx_trace is not None and tx_trace is not None:
300
+ return _plot_dual_channel_uart(
301
+ packets,
302
+ rx_trace=rx_trace,
303
+ tx_trace=tx_trace,
304
+ time_range=time_range,
305
+ time_unit=time_unit,
306
+ show_parity_errors=show_parity_errors,
307
+ show_framing_errors=show_framing_errors,
308
+ figsize=figsize,
309
+ title=title,
310
+ )
311
+
312
+ # Single-channel view using generic decode plot
313
+ return plot_protocol_decode(
314
+ packets,
315
+ trace=rx_trace or tx_trace,
316
+ trace_channel="RX" if rx_trace else "TX",
317
+ show_errors=show_parity_errors or show_framing_errors,
318
+ time_range=time_range,
319
+ time_unit=time_unit,
320
+ figsize=figsize,
321
+ title=title,
322
+ )
323
+
324
+
325
+ def _plot_dual_channel_uart(
326
+ packets: list[ProtocolPacket],
327
+ *,
328
+ rx_trace: DigitalTrace,
329
+ tx_trace: DigitalTrace,
330
+ time_range: tuple[float, float] | None = None,
331
+ time_unit: str = "auto",
332
+ show_parity_errors: bool = True,
333
+ show_framing_errors: bool = True,
334
+ figsize: tuple[float, float] | None = None,
335
+ title: str = "UART Communication",
336
+ ) -> Figure:
337
+ """Create dual-channel UART visualization with separate RX/TX rows.
338
+
339
+ Args:
340
+ packets: List of UART packets (may include both RX and TX).
341
+ rx_trace: RX digital trace.
342
+ tx_trace: TX digital trace.
343
+ time_range: Time range to plot (t_min, t_max) in seconds.
344
+ time_unit: Time unit for x-axis.
345
+ show_parity_errors: Highlight parity errors.
346
+ show_framing_errors: Highlight framing errors.
347
+ figsize: Figure size (width, height).
348
+ title: Plot title.
349
+
350
+ Returns:
351
+ Matplotlib Figure object.
352
+ """
353
+ # Determine time range from packets
354
+ if time_range is None:
355
+ t_min = min(p.timestamp for p in packets)
356
+ t_max = max(p.end_timestamp if p.end_timestamp else p.timestamp for p in packets)
357
+ padding = (t_max - t_min) * 0.1
358
+ t_min -= padding
359
+ t_max += padding
360
+ else:
361
+ t_min, t_max = time_range
362
+
363
+ # Select time unit
364
+ if time_unit == "auto":
365
+ time_range_val = t_max - t_min
366
+ if time_range_val < 1e-6:
367
+ time_unit = "ns"
368
+ time_mult = 1e9
369
+ elif time_range_val < 1e-3:
370
+ time_unit = "us"
371
+ time_mult = 1e6
372
+ elif time_range_val < 1:
373
+ time_unit = "ms"
374
+ time_mult = 1e3
375
+ else:
376
+ time_unit = "s"
377
+ time_mult = 1.0
378
+ else:
379
+ time_mult = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
380
+
381
+ # 4 rows: RX waveform, RX packets, TX waveform, TX packets
382
+ n_rows = 4
383
+
384
+ # Auto-calculate figure size
385
+ if figsize is None:
386
+ width = 14
387
+ height = max(6, n_rows * 1.2 + 1)
388
+ figsize = (width, height)
389
+
390
+ fig, axes = plt.subplots(
391
+ n_rows,
392
+ 1,
393
+ figsize=figsize,
394
+ sharex=True,
395
+ gridspec_kw={"hspace": 0.1, "height_ratios": [1, 0.8, 1, 0.8]},
396
+ )
397
+
398
+ # Separate packets by channel (using metadata if available)
399
+ rx_packets = []
400
+ tx_packets = []
401
+ for packet in packets:
402
+ # Check packet metadata for channel info
403
+ channel = getattr(packet, "channel", None)
404
+ if channel is None and hasattr(packet, "metadata"):
405
+ channel = packet.metadata.get("channel") if isinstance(packet.metadata, dict) else None
406
+
407
+ if channel == "TX":
408
+ tx_packets.append(packet)
409
+ else:
410
+ # Default to RX if channel not specified
411
+ rx_packets.append(packet)
412
+
413
+ # If no channel info, put all packets on both (as they were before)
414
+ if not rx_packets and not tx_packets:
415
+ rx_packets = packets
416
+ tx_packets = []
417
+
418
+ show_errors = show_parity_errors or show_framing_errors
419
+
420
+ # Plot RX waveform (row 0)
421
+ ax_rx_wave = axes[0]
422
+ rx_time = rx_trace.time_vector * time_mult
423
+ rx_data = rx_trace.data.astype(float)
424
+ mask = (rx_time >= t_min * time_mult) & (rx_time <= t_max * time_mult)
425
+ _plot_digital_waveform(ax_rx_wave, rx_time[mask], rx_data[mask])
426
+ ax_rx_wave.set_ylabel("RX", rotation=0, ha="right", va="center", fontsize=10)
427
+ ax_rx_wave.set_ylim(-0.2, 1.3)
428
+ ax_rx_wave.set_yticks([])
429
+ ax_rx_wave.grid(True, axis="x", alpha=0.3, linestyle=":")
430
+
431
+ # Plot RX packets (row 1)
432
+ ax_rx_packets = axes[1]
433
+ _plot_packet_row(ax_rx_packets, rx_packets, t_min, t_max, time_mult, show_errors)
434
+ ax_rx_packets.set_ylabel("RX\nData", rotation=0, ha="right", va="center", fontsize=9)
435
+
436
+ # Plot TX waveform (row 2)
437
+ ax_tx_wave = axes[2]
438
+ tx_time = tx_trace.time_vector * time_mult
439
+ tx_data = tx_trace.data.astype(float)
440
+ mask = (tx_time >= t_min * time_mult) & (tx_time <= t_max * time_mult)
441
+ _plot_digital_waveform(ax_tx_wave, tx_time[mask], tx_data[mask])
442
+ ax_tx_wave.set_ylabel("TX", rotation=0, ha="right", va="center", fontsize=10)
443
+ ax_tx_wave.set_ylim(-0.2, 1.3)
444
+ ax_tx_wave.set_yticks([])
445
+ ax_tx_wave.grid(True, axis="x", alpha=0.3, linestyle=":")
446
+
447
+ # Plot TX packets (row 3)
448
+ ax_tx_packets = axes[3]
449
+ _plot_packet_row(ax_tx_packets, tx_packets, t_min, t_max, time_mult, show_errors)
450
+ ax_tx_packets.set_ylabel("TX\nData", rotation=0, ha="right", va="center", fontsize=9)
451
+
452
+ # Set x-axis label
453
+ axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
454
+ axes[-1].set_xlim(t_min * time_mult, t_max * time_mult)
455
+
456
+ if title:
457
+ fig.suptitle(title, fontsize=14, y=0.98)
458
+
459
+ fig.tight_layout()
460
+ return fig
461
+
462
+
463
+ def _plot_packet_row(
464
+ ax: Axes,
465
+ packets: list[ProtocolPacket],
466
+ t_min: float,
467
+ t_max: float,
468
+ time_mult: float,
469
+ show_errors: bool,
470
+ ) -> None:
471
+ """Plot a single row of packets on the given axes."""
472
+ for packet in packets:
473
+ if packet.timestamp < t_min or packet.timestamp > t_max:
474
+ continue
475
+
476
+ start = packet.timestamp * time_mult
477
+ end = (
478
+ packet.end_timestamp if packet.end_timestamp else packet.timestamp + 0.001
479
+ ) * time_mult
480
+
481
+ # Determine packet color
482
+ if show_errors and packet.errors:
483
+ color = "#ff6b6b" # Red for errors
484
+ else:
485
+ color = "#4ecdc4" # Teal
486
+
487
+ # Draw packet rectangle
488
+ rect = patches.Rectangle(
489
+ (start, 0.1),
490
+ end - start,
491
+ 0.8,
492
+ facecolor=color,
493
+ edgecolor="black",
494
+ linewidth=0.8,
495
+ alpha=0.7,
496
+ )
497
+ ax.add_patch(rect)
498
+
499
+ # Add data annotation
500
+ if packet.data:
501
+ data_str = _format_packet_data(packet)
502
+ mid_time = (start + end) / 2
503
+ ax.text(
504
+ mid_time,
505
+ 0.5,
506
+ data_str,
507
+ ha="center",
508
+ va="center",
509
+ fontsize=7,
510
+ fontweight="bold",
511
+ color="white" if not (show_errors and packet.errors) else "black",
512
+ )
513
+
514
+ # Add error markers
515
+ if show_errors and packet.errors:
516
+ ax.plot(start, 1.1, "rx", markersize=6, markeredgewidth=2)
517
+
518
+ ax.set_ylim(0, 1.2)
519
+ ax.set_yticks([])
520
+ ax.grid(True, axis="x", alpha=0.3, linestyle=":")
521
+
522
+
523
+ def plot_spi_decode(
524
+ packets: list[ProtocolPacket],
525
+ *,
526
+ clk_trace: DigitalTrace | None = None,
527
+ mosi_trace: DigitalTrace | None = None,
528
+ miso_trace: DigitalTrace | None = None,
529
+ cs_trace: DigitalTrace | None = None,
530
+ time_range: tuple[float, float] | None = None,
531
+ time_unit: str = "auto",
532
+ show_mosi: bool = True,
533
+ show_miso: bool = True,
534
+ figsize: tuple[float, float] | None = None,
535
+ title: str = "SPI Transaction",
536
+ ) -> Figure:
537
+ """Plot SPI decoded packets with CLK, MOSI, MISO, CS signals.
538
+
539
+ Specialized visualization for SPI showing all relevant signals
540
+ and decoded words on MOSI/MISO channels.
541
+
542
+ Args:
543
+ packets: List of SPI packets.
544
+ clk_trace: Optional clock signal trace.
545
+ mosi_trace: Optional MOSI (Master Out Slave In) trace.
546
+ miso_trace: Optional MISO (Master In Slave Out) trace.
547
+ cs_trace: Optional chip select trace.
548
+ time_range: Time range to plot (t_min, t_max) in seconds.
549
+ time_unit: Time unit for x-axis.
550
+ show_mosi: Show MOSI decoded data.
551
+ show_miso: Show MISO decoded data.
552
+ figsize: Figure size.
553
+ title: Plot title.
554
+
555
+ Returns:
556
+ Matplotlib Figure object.
557
+
558
+ Raises:
559
+ ImportError: If matplotlib is not installed.
560
+ ValueError: If packets list is empty.
561
+
562
+ Example:
563
+ >>> decoder = SPIDecoder(cpol=0, cpha=0, word_size=8)
564
+ >>> packets = list(decoder.decode(clk=clk, mosi=mosi, miso=miso))
565
+ >>> fig = plot_spi_decode(packets, clk_trace=clk, mosi_trace=mosi)
566
+ """
567
+ if not HAS_MATPLOTLIB:
568
+ raise ImportError("matplotlib is required for visualization")
569
+
570
+ if len(packets) == 0:
571
+ raise ValueError("packets list cannot be empty")
572
+
573
+ # If we have multiple traces, create multi-channel visualization
574
+ traces_available = sum(
575
+ 1 for t in [clk_trace, mosi_trace, miso_trace, cs_trace] if t is not None
576
+ )
577
+
578
+ if traces_available >= 2:
579
+ return _plot_multi_channel_spi(
580
+ packets,
581
+ clk_trace=clk_trace,
582
+ mosi_trace=mosi_trace,
583
+ miso_trace=miso_trace,
584
+ cs_trace=cs_trace,
585
+ time_range=time_range,
586
+ time_unit=time_unit,
587
+ show_mosi=show_mosi,
588
+ show_miso=show_miso,
589
+ figsize=figsize,
590
+ title=title,
591
+ )
592
+
593
+ # Single-channel view using generic decode plot
594
+ return plot_protocol_decode(
595
+ packets,
596
+ trace=mosi_trace,
597
+ trace_channel="MOSI",
598
+ time_range=time_range,
599
+ time_unit=time_unit,
600
+ figsize=figsize,
601
+ title=title,
602
+ )
603
+
604
+
605
+ def _plot_multi_channel_spi(
606
+ packets: list[ProtocolPacket],
607
+ *,
608
+ clk_trace: DigitalTrace | None = None,
609
+ mosi_trace: DigitalTrace | None = None,
610
+ miso_trace: DigitalTrace | None = None,
611
+ cs_trace: DigitalTrace | None = None,
612
+ time_range: tuple[float, float] | None = None,
613
+ time_unit: str = "auto",
614
+ show_mosi: bool = True,
615
+ show_miso: bool = True,
616
+ figsize: tuple[float, float] | None = None,
617
+ title: str = "SPI Transaction",
618
+ ) -> Figure:
619
+ """Create multi-channel SPI visualization with separate rows for each signal.
620
+
621
+ Args:
622
+ packets: List of SPI packets.
623
+ clk_trace: Optional clock signal trace.
624
+ mosi_trace: Optional MOSI trace.
625
+ miso_trace: Optional MISO trace.
626
+ cs_trace: Optional chip select trace.
627
+ time_range: Time range to plot.
628
+ time_unit: Time unit for x-axis.
629
+ show_mosi: Show MOSI decoded data row.
630
+ show_miso: Show MISO decoded data row.
631
+ figsize: Figure size.
632
+ title: Plot title.
633
+
634
+ Returns:
635
+ Matplotlib Figure object.
636
+ """
637
+ # Determine time range from packets
638
+ if time_range is None:
639
+ t_min = min(p.timestamp for p in packets)
640
+ t_max = max(p.end_timestamp if p.end_timestamp else p.timestamp for p in packets)
641
+ padding = (t_max - t_min) * 0.1
642
+ t_min -= padding
643
+ t_max += padding
644
+ else:
645
+ t_min, t_max = time_range
646
+
647
+ # Select time unit
648
+ if time_unit == "auto":
649
+ time_range_val = t_max - t_min
650
+ if time_range_val < 1e-6:
651
+ time_unit = "ns"
652
+ time_mult = 1e9
653
+ elif time_range_val < 1e-3:
654
+ time_unit = "us"
655
+ time_mult = 1e6
656
+ elif time_range_val < 1:
657
+ time_unit = "ms"
658
+ time_mult = 1e3
659
+ else:
660
+ time_unit = "s"
661
+ time_mult = 1.0
662
+ else:
663
+ time_mult = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
664
+
665
+ # Build list of rows to display
666
+ rows: list[dict[str, Any]] = []
667
+
668
+ if cs_trace is not None:
669
+ rows.append({"type": "waveform", "trace": cs_trace, "label": "CS"})
670
+
671
+ if clk_trace is not None:
672
+ rows.append({"type": "waveform", "trace": clk_trace, "label": "CLK"})
673
+
674
+ if mosi_trace is not None:
675
+ rows.append({"type": "waveform", "trace": mosi_trace, "label": "MOSI"})
676
+ if show_mosi:
677
+ rows.append({"type": "packets", "label": "MOSI\nData", "channel": "MOSI"})
678
+
679
+ if miso_trace is not None:
680
+ rows.append({"type": "waveform", "trace": miso_trace, "label": "MISO"})
681
+ if show_miso:
682
+ rows.append({"type": "packets", "label": "MISO\nData", "channel": "MISO"})
683
+
684
+ n_rows = len(rows)
685
+ if n_rows == 0:
686
+ # Fallback to generic if no traces
687
+ return plot_protocol_decode(
688
+ packets,
689
+ time_range=time_range,
690
+ time_unit=time_unit,
691
+ figsize=figsize,
692
+ title=title,
693
+ )
694
+
695
+ # Calculate height ratios (waveforms get more space than data rows)
696
+ height_ratios = []
697
+ for row in rows:
698
+ if row["type"] == "waveform":
699
+ height_ratios.append(1.0)
700
+ else:
701
+ height_ratios.append(0.6)
702
+
703
+ # Auto-calculate figure size
704
+ if figsize is None:
705
+ width = 14
706
+ height = max(4, sum(height_ratios) * 1.2 + 1)
707
+ figsize = (width, height)
708
+
709
+ fig, axes = plt.subplots(
710
+ n_rows,
711
+ 1,
712
+ figsize=figsize,
713
+ sharex=True,
714
+ gridspec_kw={"hspace": 0.1, "height_ratios": height_ratios},
715
+ )
716
+
717
+ if n_rows == 1:
718
+ axes = [axes]
719
+
720
+ # Separate packets by channel (MOSI vs MISO)
721
+ mosi_packets = []
722
+ miso_packets = []
723
+ for packet in packets:
724
+ channel = getattr(packet, "channel", None)
725
+ if channel is None and hasattr(packet, "metadata"):
726
+ channel = packet.metadata.get("channel") if isinstance(packet.metadata, dict) else None
727
+
728
+ if channel == "MISO":
729
+ miso_packets.append(packet)
730
+ else:
731
+ # Default to MOSI
732
+ mosi_packets.append(packet)
733
+
734
+ # If no channel info, use all packets for MOSI
735
+ if not mosi_packets and not miso_packets:
736
+ mosi_packets = packets
737
+
738
+ # Plot each row
739
+ for ax, row in zip(axes, rows, strict=False):
740
+ if row["type"] == "waveform":
741
+ trace = row["trace"]
742
+ trace_time = trace.time_vector * time_mult
743
+ trace_data = trace.data.astype(float)
744
+ mask = (trace_time >= t_min * time_mult) & (trace_time <= t_max * time_mult)
745
+ _plot_digital_waveform(ax, trace_time[mask], trace_data[mask])
746
+ ax.set_ylabel(row["label"], rotation=0, ha="right", va="center", fontsize=10)
747
+ ax.set_ylim(-0.2, 1.3)
748
+ ax.set_yticks([])
749
+ ax.grid(True, axis="x", alpha=0.3, linestyle=":")
750
+ else:
751
+ # Packet row
752
+ channel = row.get("channel", "MOSI")
753
+ pkts = mosi_packets if channel == "MOSI" else miso_packets
754
+ _plot_packet_row(ax, pkts, t_min, t_max, time_mult, show_errors=True)
755
+ ax.set_ylabel(row["label"], rotation=0, ha="right", va="center", fontsize=9)
756
+
757
+ # Set x-axis label
758
+ axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
759
+ axes[-1].set_xlim(t_min * time_mult, t_max * time_mult)
760
+
761
+ if title:
762
+ fig.suptitle(title, fontsize=14, y=0.98)
763
+
764
+ fig.tight_layout()
765
+ return fig
766
+
767
+
768
+ def plot_i2c_decode(
769
+ packets: list[ProtocolPacket],
770
+ *,
771
+ sda_trace: DigitalTrace | None = None,
772
+ scl_trace: DigitalTrace | None = None,
773
+ time_range: tuple[float, float] | None = None,
774
+ time_unit: str = "auto",
775
+ show_addresses: bool = True,
776
+ show_ack_nack: bool = True,
777
+ figsize: tuple[float, float] | None = None,
778
+ title: str = "I2C Transaction",
779
+ ) -> Figure:
780
+ """Plot I2C decoded packets with SDA/SCL and address annotations.
781
+
782
+ Specialized visualization for I2C showing start/stop conditions,
783
+ addresses, data bytes, and ACK/NACK bits.
784
+
785
+ Args:
786
+ packets: List of I2C packets.
787
+ sda_trace: Optional SDA (data) signal trace.
788
+ scl_trace: Optional SCL (clock) signal trace.
789
+ time_range: Time range to plot (t_min, t_max) in seconds.
790
+ time_unit: Time unit for x-axis.
791
+ show_addresses: Highlight address bytes.
792
+ show_ack_nack: Show ACK/NACK indicators.
793
+ figsize: Figure size.
794
+ title: Plot title.
795
+
796
+ Returns:
797
+ Matplotlib Figure object.
798
+
799
+ Example:
800
+ >>> decoder = I2CDecoder()
801
+ >>> packets = list(decoder.decode(sda=sda, scl=scl))
802
+ >>> fig = plot_i2c_decode(packets, sda_trace=sda, scl_trace=scl)
803
+ """
804
+ return plot_protocol_decode(
805
+ packets,
806
+ trace=sda_trace,
807
+ trace_channel="SDA",
808
+ time_range=time_range,
809
+ time_unit=time_unit,
810
+ figsize=figsize,
811
+ title=title,
812
+ )
813
+
814
+
815
+ def plot_can_decode(
816
+ packets: list[ProtocolPacket],
817
+ *,
818
+ can_trace: DigitalTrace | None = None,
819
+ time_range: tuple[float, float] | None = None,
820
+ time_unit: str = "auto",
821
+ show_ids: bool = True,
822
+ show_data_length: bool = True,
823
+ colorize_by_id: bool = True,
824
+ figsize: tuple[float, float] | None = None,
825
+ title: str = "CAN Bus",
826
+ ) -> Figure:
827
+ """Plot CAN decoded packets with arbitration IDs and data.
828
+
829
+ Specialized visualization for CAN bus showing arbitration IDs,
830
+ data length codes, and message data.
831
+
832
+ Args:
833
+ packets: List of CAN packets.
834
+ can_trace: Optional CAN bus trace.
835
+ time_range: Time range to plot (t_min, t_max) in seconds.
836
+ time_unit: Time unit for x-axis.
837
+ show_ids: Show arbitration IDs in annotations.
838
+ show_data_length: Show DLC (Data Length Code).
839
+ colorize_by_id: Use different colors for different CAN IDs.
840
+ figsize: Figure size.
841
+ title: Plot title.
842
+
843
+ Returns:
844
+ Matplotlib Figure object.
845
+
846
+ Example:
847
+ >>> decoder = CANDecoder()
848
+ >>> packets = list(decoder.decode(can_trace))
849
+ >>> fig = plot_can_decode(packets, can_trace=can_trace, colorize_by_id=True)
850
+ """
851
+ return plot_protocol_decode(
852
+ packets,
853
+ trace=can_trace,
854
+ trace_channel="CAN",
855
+ colorize=colorize_by_id,
856
+ time_range=time_range,
857
+ time_unit=time_unit,
858
+ figsize=figsize,
859
+ title=title,
860
+ )
861
+
862
+
863
+ def _plot_digital_waveform(
864
+ ax: Axes,
865
+ time: NDArray[np.float64],
866
+ data: NDArray[np.float64],
867
+ ) -> None:
868
+ """Plot digital waveform with clean transitions."""
869
+ for i in range(len(time) - 1):
870
+ level = 1 if data[i] > 0.5 else 0
871
+ # Horizontal line
872
+ ax.plot(
873
+ [time[i], time[i + 1]],
874
+ [level, level],
875
+ "b-",
876
+ linewidth=1.5,
877
+ )
878
+ # Vertical transition
879
+ if i < len(time) - 1:
880
+ next_level = 1 if data[i + 1] > 0.5 else 0
881
+ if level != next_level:
882
+ ax.plot(
883
+ [time[i + 1], time[i + 1]],
884
+ [level, next_level],
885
+ "b-",
886
+ linewidth=1.5,
887
+ )
888
+
889
+
890
+ def _get_packet_color(packet: ProtocolPacket, protocol: str) -> str:
891
+ """Get color for packet based on protocol and type."""
892
+ # Color palette for different protocols
893
+ colors = {
894
+ "UART": "#4ecdc4", # Teal
895
+ "SPI": "#95e1d3", # Mint
896
+ "I2C": "#f38181", # Coral
897
+ "CAN": "#aa96da", # Purple
898
+ "USB": "#fcbad3", # Pink
899
+ "1-Wire": "#ffffd2", # Yellow
900
+ }
901
+
902
+ return colors.get(protocol, "#4ecdc4")
903
+
904
+
905
+ def _format_packet_data(packet: ProtocolPacket) -> str:
906
+ """Format packet data for display."""
907
+ if len(packet.data) == 0:
908
+ return ""
909
+
910
+ # For single byte, show as hex
911
+ if len(packet.data) == 1:
912
+ byte_val = packet.data[0]
913
+ # Show both hex and ASCII if printable
914
+ if 32 <= byte_val <= 126:
915
+ return f"0x{byte_val:02X} '{chr(byte_val)}'"
916
+ return f"0x{byte_val:02X}"
917
+
918
+ # For multiple bytes, show hex string (limit to first few bytes)
919
+ if len(packet.data) <= 4:
920
+ return " ".join(f"{b:02X}" for b in packet.data)
921
+
922
+ # For longer data, truncate
923
+ return " ".join(f"{b:02X}" for b in packet.data[:3]) + "..."
924
+
925
+
926
+ __all__ = [
927
+ "plot_can_decode",
928
+ "plot_i2c_decode",
929
+ "plot_protocol_decode",
930
+ "plot_spi_decode",
931
+ "plot_uart_decode",
932
+ ]