oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (497) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -92,27 +92,61 @@ def plot_protocol_decode(
92
92
  References:
93
93
  VIS-030: Protocol Decode Visualization
94
94
  """
95
+ _validate_plot_inputs(packets)
96
+ protocol = packets[0].protocol
97
+ t_min, t_max, time_mult, time_unit = _calculate_time_parameters(packets, time_range, time_unit)
98
+ fig, axes = _create_figure_layout(trace, figsize)
99
+ ax_idx = _plot_waveform_if_present(
100
+ axes, trace, trace_channel, protocol, t_min, t_max, time_mult
101
+ )
102
+ _plot_packet_timeline(
103
+ axes[ax_idx], packets, protocol, t_min, t_max, time_mult, show_data, show_errors, colorize
104
+ )
105
+ _finalize_plot_layout(axes, t_min, t_max, time_mult, time_unit, title)
106
+
107
+ return fig
108
+
109
+
110
+ def _validate_plot_inputs(packets: list[ProtocolPacket]) -> None:
111
+ """Validate plot inputs.
112
+
113
+ Args:
114
+ packets: List of protocol packets.
115
+
116
+ Raises:
117
+ ImportError: If matplotlib not available.
118
+ ValueError: If packets list is empty.
119
+ """
95
120
  if not HAS_MATPLOTLIB:
96
121
  raise ImportError("matplotlib is required for visualization")
97
-
98
122
  if len(packets) == 0:
99
123
  raise ValueError("packets list cannot be empty")
100
124
 
101
- # Determine protocol name from first packet
102
- protocol = packets[0].protocol
103
125
 
104
- # Determine time range
126
+ def _calculate_time_parameters(
127
+ packets: list[ProtocolPacket],
128
+ time_range: tuple[float, float] | None,
129
+ time_unit: str,
130
+ ) -> tuple[float, float, float, str]:
131
+ """Calculate time range and multiplier for plotting.
132
+
133
+ Args:
134
+ packets: List of packets for auto time range.
135
+ time_range: User-specified time range or None.
136
+ time_unit: Time unit string.
137
+
138
+ Returns:
139
+ Tuple of (t_min, t_max, time_mult, time_unit).
140
+ """
105
141
  if time_range is None:
106
142
  t_min = min(p.timestamp for p in packets)
107
143
  t_max = max(p.end_timestamp if p.end_timestamp else p.timestamp for p in packets)
108
- # Add 10% padding
109
144
  padding = (t_max - t_min) * 0.1
110
145
  t_min -= padding
111
146
  t_max += padding
112
147
  else:
113
148
  t_min, t_max = time_range
114
149
 
115
- # Select time unit
116
150
  if time_unit == "auto":
117
151
  time_range_val = t_max - t_min
118
152
  if time_range_val < 1e-6:
@@ -130,10 +164,23 @@ def plot_protocol_decode(
130
164
  else:
131
165
  time_mult = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
132
166
 
133
- # Determine number of rows
134
- n_rows = 1 if trace is None else 2 # Waveform + packets
167
+ return t_min, t_max, time_mult, time_unit
168
+
169
+
170
+ def _create_figure_layout(
171
+ trace: DigitalTrace | None, figsize: tuple[float, float] | None
172
+ ) -> tuple[Figure, list[Axes]]:
173
+ """Create figure and axes layout.
174
+
175
+ Args:
176
+ trace: Optional trace for determining row count.
177
+ figsize: Figure size or None for auto.
178
+
179
+ Returns:
180
+ Tuple of (figure, axes_list).
181
+ """
182
+ n_rows = 1 if trace is None else 2
135
183
 
136
- # Auto-calculate figure size
137
184
  if figsize is None:
138
185
  width = 14
139
186
  height = max(4, n_rows * 1.5 + 1)
@@ -150,35 +197,78 @@ def plot_protocol_decode(
150
197
  if n_rows == 1:
151
198
  axes = [axes]
152
199
 
153
- ax_idx = 0
200
+ return fig, axes
154
201
 
155
- # Plot waveform if provided
156
- if trace is not None:
157
- ax = axes[ax_idx]
158
- ax_idx += 1
159
202
 
160
- # Create time vector for trace
161
- trace_time = trace.time_vector * time_mult
162
- trace_data = trace.data.astype(float)
203
+ def _plot_waveform_if_present(
204
+ axes: list[Axes],
205
+ trace: DigitalTrace | None,
206
+ trace_channel: str | None,
207
+ protocol: str,
208
+ t_min: float,
209
+ t_max: float,
210
+ time_mult: float,
211
+ ) -> int:
212
+ """Plot waveform trace if provided.
163
213
 
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]
214
+ Args:
215
+ axes: List of axes to plot on.
216
+ trace: Optional digital trace.
217
+ trace_channel: Channel name override.
218
+ protocol: Protocol name for default label.
219
+ t_min: Minimum time value.
220
+ t_max: Maximum time value.
221
+ time_mult: Time unit multiplier.
168
222
 
169
- # Plot as digital waveform
170
- _plot_digital_waveform(ax, trace_time, trace_data)
223
+ Returns:
224
+ Index of next available axis.
225
+ """
226
+ if trace is None:
227
+ return 0
228
+
229
+ ax = axes[0]
230
+ trace_time = trace.time_vector * time_mult
231
+ trace_data = trace.data.astype(float)
232
+
233
+ mask = (trace_time >= t_min * time_mult) & (trace_time <= t_max * time_mult)
234
+ trace_time = trace_time[mask]
235
+ trace_data = trace_data[mask]
236
+
237
+ _plot_digital_waveform(ax, trace_time, trace_data)
238
+
239
+ channel_name = trace_channel if trace_channel else protocol
240
+ ax.set_ylabel(channel_name, rotation=0, ha="right", va="center", fontsize=10)
241
+ ax.set_ylim(-0.2, 1.3)
242
+ ax.set_yticks([])
243
+ ax.grid(True, axis="x", alpha=0.3, linestyle=":")
244
+
245
+ return 1
171
246
 
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
247
 
178
- # Plot packets row
179
- ax = axes[ax_idx]
248
+ def _plot_packet_timeline(
249
+ ax: Axes,
250
+ packets: list[ProtocolPacket],
251
+ protocol: str,
252
+ t_min: float,
253
+ t_max: float,
254
+ time_mult: float,
255
+ show_data: bool,
256
+ show_errors: bool,
257
+ colorize: bool,
258
+ ) -> None:
259
+ """Plot packet timeline on axis.
180
260
 
181
- # Plot packet timeline
261
+ Args:
262
+ ax: Matplotlib axis.
263
+ packets: List of packets to plot.
264
+ protocol: Protocol name.
265
+ t_min: Minimum time value.
266
+ t_max: Maximum time value.
267
+ time_mult: Time unit multiplier.
268
+ show_data: Show data annotations.
269
+ show_errors: Highlight errors.
270
+ colorize: Use color coding.
271
+ """
182
272
  for packet in packets:
183
273
  if packet.timestamp < t_min or packet.timestamp > t_max:
184
274
  continue
@@ -188,15 +278,8 @@ def plot_protocol_decode(
188
278
  packet.end_timestamp if packet.end_timestamp else packet.timestamp + 0.001
189
279
  ) * time_mult
190
280
 
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
281
+ color = _determine_packet_color(packet, protocol, show_errors, colorize)
198
282
 
199
- # Draw packet rectangle
200
283
  rect = patches.Rectangle(
201
284
  (start, 0.1),
202
285
  end - start,
@@ -208,45 +291,93 @@ def plot_protocol_decode(
208
291
  )
209
292
  ax.add_patch(rect)
210
293
 
211
- # Add data annotation
212
294
  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
- )
295
+ _add_packet_annotation(ax, packet, start, end, show_errors)
225
296
 
226
- # Add error markers
227
297
  if show_errors and packet.errors:
228
- ax.plot(
229
- start,
230
- 1.1,
231
- "rx",
232
- markersize=8,
233
- markeredgewidth=2,
234
- )
298
+ ax.plot(start, 1.1, "rx", markersize=8, markeredgewidth=2)
235
299
 
236
300
  ax.set_ylabel(f"{protocol}\nPackets", rotation=0, ha="right", va="center", fontsize=10)
237
301
  ax.set_ylim(0, 1.2)
238
302
  ax.set_yticks([])
239
303
  ax.grid(True, axis="x", alpha=0.3, linestyle=":")
240
304
 
241
- # Set x-axis label
305
+
306
+ def _determine_packet_color(
307
+ packet: ProtocolPacket, protocol: str, show_errors: bool, colorize: bool
308
+ ) -> str:
309
+ """Determine packet rectangle color.
310
+
311
+ Args:
312
+ packet: Protocol packet.
313
+ protocol: Protocol name.
314
+ show_errors: Whether to highlight errors.
315
+ colorize: Whether to use protocol colors.
316
+
317
+ Returns:
318
+ Color string.
319
+ """
320
+ if show_errors and packet.errors:
321
+ return "#ff6b6b"
322
+ elif colorize:
323
+ return _get_packet_color(packet, protocol)
324
+ else:
325
+ return "#4ecdc4"
326
+
327
+
328
+ def _add_packet_annotation(
329
+ ax: Axes, packet: ProtocolPacket, start: float, end: float, show_errors: bool
330
+ ) -> None:
331
+ """Add data annotation to packet.
332
+
333
+ Args:
334
+ ax: Matplotlib axis.
335
+ packet: Protocol packet.
336
+ start: Start time in scaled units.
337
+ end: End time in scaled units.
338
+ show_errors: Whether errors are highlighted.
339
+ """
340
+ data_str = _format_packet_data(packet)
341
+ mid_time = (start + end) / 2
342
+ text_color = "white" if not (show_errors and packet.errors) else "black"
343
+ ax.text(
344
+ mid_time,
345
+ 0.5,
346
+ data_str,
347
+ ha="center",
348
+ va="center",
349
+ fontsize=8,
350
+ fontweight="bold",
351
+ color=text_color,
352
+ )
353
+
354
+
355
+ def _finalize_plot_layout(
356
+ axes: list[Axes],
357
+ t_min: float,
358
+ t_max: float,
359
+ time_mult: float,
360
+ time_unit: str,
361
+ title: str | None,
362
+ ) -> None:
363
+ """Finalize plot layout with labels and title.
364
+
365
+ Args:
366
+ axes: List of axes.
367
+ t_min: Minimum time value.
368
+ t_max: Maximum time value.
369
+ time_mult: Time unit multiplier.
370
+ time_unit: Time unit string.
371
+ title: Optional plot title.
372
+ """
242
373
  axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
243
374
  axes[-1].set_xlim(t_min * time_mult, t_max * time_mult)
244
375
 
245
- if title:
246
- fig.suptitle(title, fontsize=14, y=0.98)
247
-
248
- fig.tight_layout()
249
- return fig
376
+ fig = axes[0].get_figure()
377
+ if fig and hasattr(fig, "tight_layout"):
378
+ if title:
379
+ fig.suptitle(title, fontsize=14, y=0.98)
380
+ fig.tight_layout()
250
381
 
251
382
 
252
383
  def plot_uart_decode(
@@ -350,38 +481,43 @@ def _plot_dual_channel_uart(
350
481
  Returns:
351
482
  Matplotlib Figure object.
352
483
  """
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
484
+ # Calculate time parameters
485
+ t_min, t_max, time_mult, time_unit = _determine_time_params(packets, time_range, time_unit)
362
486
 
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)
487
+ # Create figure with 4 rows
488
+ fig, axes = _create_dual_uart_figure(figsize)
489
+
490
+ # Separate packets by channel
491
+ rx_packets, tx_packets = _separate_uart_packets(packets)
492
+ show_errors = show_parity_errors or show_framing_errors
493
+
494
+ # Plot all four rows
495
+ _plot_uart_channel_pair(
496
+ axes[0], axes[1], rx_trace, rx_packets, "RX", t_min, t_max, time_mult, show_errors
497
+ )
498
+ _plot_uart_channel_pair(
499
+ axes[2], axes[3], tx_trace, tx_packets, "TX", t_min, t_max, time_mult, show_errors
500
+ )
501
+
502
+ # Finalize plot
503
+ _finalize_uart_plot(fig, axes, t_min, t_max, time_mult, time_unit, title)
504
+
505
+ return fig
380
506
 
381
- # 4 rows: RX waveform, RX packets, TX waveform, TX packets
507
+
508
+ def _create_dual_uart_figure(
509
+ figsize: tuple[float, float] | None,
510
+ ) -> tuple[Figure, list[Axes]]:
511
+ """Create figure for dual-channel UART plot.
512
+
513
+ Args:
514
+ figsize: Figure size or None for auto-calculation.
515
+
516
+ Returns:
517
+ Tuple of (figure, axes_list).
518
+ """
382
519
  n_rows = 4
383
520
 
384
- # Auto-calculate figure size
385
521
  if figsize is None:
386
522
  width = 14
387
523
  height = max(6, n_rows * 1.2 + 1)
@@ -395,11 +531,24 @@ def _plot_dual_channel_uart(
395
531
  gridspec_kw={"hspace": 0.1, "height_ratios": [1, 0.8, 1, 0.8]},
396
532
  )
397
533
 
398
- # Separate packets by channel (using metadata if available)
534
+ return fig, axes
535
+
536
+
537
+ def _separate_uart_packets(
538
+ packets: list[ProtocolPacket],
539
+ ) -> tuple[list[ProtocolPacket], list[ProtocolPacket]]:
540
+ """Separate UART packets by channel (RX vs TX).
541
+
542
+ Args:
543
+ packets: List of UART packets.
544
+
545
+ Returns:
546
+ Tuple of (rx_packets, tx_packets).
547
+ """
399
548
  rx_packets = []
400
549
  tx_packets = []
550
+
401
551
  for packet in packets:
402
- # Check packet metadata for channel info
403
552
  channel = getattr(packet, "channel", None)
404
553
  if channel is None and hasattr(packet, "metadata"):
405
554
  channel = packet.metadata.get("channel") if isinstance(packet.metadata, dict) else None
@@ -407,49 +556,74 @@ def _plot_dual_channel_uart(
407
556
  if channel == "TX":
408
557
  tx_packets.append(packet)
409
558
  else:
410
- # Default to RX if channel not specified
411
559
  rx_packets.append(packet)
412
560
 
413
- # If no channel info, put all packets on both (as they were before)
561
+ # If no channel info, put all packets on RX
414
562
  if not rx_packets and not tx_packets:
415
563
  rx_packets = packets
416
- tx_packets = []
417
564
 
418
- show_errors = show_parity_errors or show_framing_errors
565
+ return rx_packets, tx_packets
419
566
 
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
567
+
568
+ def _plot_uart_channel_pair(
569
+ ax_wave: Axes,
570
+ ax_packets: Axes,
571
+ trace: DigitalTrace,
572
+ packets: list[ProtocolPacket],
573
+ label: str,
574
+ t_min: float,
575
+ t_max: float,
576
+ time_mult: float,
577
+ show_errors: bool,
578
+ ) -> None:
579
+ """Plot waveform and packet row for a single UART channel.
580
+
581
+ Args:
582
+ ax_wave: Axis for waveform plot.
583
+ ax_packets: Axis for packet annotations.
584
+ trace: Digital trace for the channel.
585
+ packets: Packets for this channel.
586
+ label: Channel label (e.g., "RX" or "TX").
587
+ t_min: Minimum time value.
588
+ t_max: Maximum time value.
589
+ time_mult: Time unit multiplier.
590
+ show_errors: Whether to highlight errors.
591
+ """
592
+ # Plot waveform
593
+ trace_time = trace.time_vector * time_mult
594
+ trace_data = trace.data.astype(float)
595
+ mask = (trace_time >= t_min * time_mult) & (trace_time <= t_max * time_mult)
596
+ _plot_digital_waveform(ax_wave, trace_time[mask], trace_data[mask])
597
+ ax_wave.set_ylabel(label, rotation=0, ha="right", va="center", fontsize=10)
598
+ ax_wave.set_ylim(-0.2, 1.3)
599
+ ax_wave.set_yticks([])
600
+ ax_wave.grid(True, axis="x", alpha=0.3, linestyle=":")
601
+
602
+ # Plot packets
603
+ _plot_packet_row(ax_packets, packets, t_min, t_max, time_mult, show_errors)
604
+ ax_packets.set_ylabel(f"{label}\nData", rotation=0, ha="right", va="center", fontsize=9)
605
+
606
+
607
+ def _finalize_uart_plot(
608
+ fig: Figure,
609
+ axes: list[Axes],
610
+ t_min: float,
611
+ t_max: float,
612
+ time_mult: float,
613
+ time_unit: str,
614
+ title: str | None,
615
+ ) -> None:
616
+ """Add final formatting to UART plot.
617
+
618
+ Args:
619
+ fig: Matplotlib figure.
620
+ axes: List of axes.
621
+ t_min: Minimum time value.
622
+ t_max: Maximum time value.
623
+ time_mult: Time multiplier.
624
+ time_unit: Time unit string.
625
+ title: Plot title or None.
626
+ """
453
627
  axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
454
628
  axes[-1].set_xlim(t_min * time_mult, t_max * time_mult)
455
629
 
@@ -457,7 +631,6 @@ def _plot_dual_channel_uart(
457
631
  fig.suptitle(title, fontsize=14, y=0.98)
458
632
 
459
633
  fig.tight_layout()
460
- return fig
461
634
 
462
635
 
463
636
  def _plot_packet_row(
@@ -634,7 +807,37 @@ def _plot_multi_channel_spi(
634
807
  Returns:
635
808
  Matplotlib Figure object.
636
809
  """
637
- # Determine time range from packets
810
+ t_min, t_max, time_mult, time_unit = _determine_time_params(packets, time_range, time_unit)
811
+ rows = _build_spi_row_list(cs_trace, clk_trace, mosi_trace, miso_trace, show_mosi, show_miso)
812
+
813
+ if len(rows) == 0:
814
+ return plot_protocol_decode(
815
+ packets, time_range=(t_min, t_max), time_unit=time_unit, figsize=figsize, title=title
816
+ )
817
+
818
+ fig, axes = _create_spi_figure(rows, figsize)
819
+ mosi_packets, miso_packets = _separate_spi_packets(packets)
820
+ _render_spi_rows(axes, rows, t_min, t_max, time_mult, mosi_packets, miso_packets)
821
+ _finalize_spi_plot(axes, t_min, t_max, time_mult, time_unit, title)
822
+
823
+ return fig
824
+
825
+
826
+ def _determine_time_params(
827
+ packets: list[ProtocolPacket],
828
+ time_range: tuple[float, float] | None,
829
+ time_unit: str,
830
+ ) -> tuple[float, float, float, str]:
831
+ """Determine time range and multiplier for SPI plot.
832
+
833
+ Args:
834
+ packets: List of SPI packets for time range calculation.
835
+ time_range: User-specified time range or None for auto.
836
+ time_unit: Time unit ("auto" or specific unit).
837
+
838
+ Returns:
839
+ Tuple of (t_min, t_max, time_mult, time_unit).
840
+ """
638
841
  if time_range is None:
639
842
  t_min = min(p.timestamp for p in packets)
640
843
  t_max = max(p.end_timestamp if p.end_timestamp else p.timestamp for p in packets)
@@ -644,7 +847,6 @@ def _plot_multi_channel_spi(
644
847
  else:
645
848
  t_min, t_max = time_range
646
849
 
647
- # Select time unit
648
850
  if time_unit == "auto":
649
851
  time_range_val = t_max - t_min
650
852
  if time_range_val < 1e-6:
@@ -662,45 +864,64 @@ def _plot_multi_channel_spi(
662
864
  else:
663
865
  time_mult = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
664
866
 
665
- # Build list of rows to display
867
+ return t_min, t_max, time_mult, time_unit
868
+
869
+
870
+ def _build_spi_row_list(
871
+ cs_trace: DigitalTrace | None,
872
+ clk_trace: DigitalTrace | None,
873
+ mosi_trace: DigitalTrace | None,
874
+ miso_trace: DigitalTrace | None,
875
+ show_mosi: bool,
876
+ show_miso: bool,
877
+ ) -> list[dict[str, Any]]:
878
+ """Build list of row specifications for SPI multi-channel plot.
879
+
880
+ Args:
881
+ cs_trace: Chip select trace.
882
+ clk_trace: Clock trace.
883
+ mosi_trace: MOSI trace.
884
+ miso_trace: MISO trace.
885
+ show_mosi: Whether to show MOSI data row.
886
+ show_miso: Whether to show MISO data row.
887
+
888
+ Returns:
889
+ List of row dictionaries specifying type, trace, label, channel.
890
+ """
666
891
  rows: list[dict[str, Any]] = []
667
892
 
668
893
  if cs_trace is not None:
669
894
  rows.append({"type": "waveform", "trace": cs_trace, "label": "CS"})
670
-
671
895
  if clk_trace is not None:
672
896
  rows.append({"type": "waveform", "trace": clk_trace, "label": "CLK"})
673
-
674
897
  if mosi_trace is not None:
675
898
  rows.append({"type": "waveform", "trace": mosi_trace, "label": "MOSI"})
676
899
  if show_mosi:
677
900
  rows.append({"type": "packets", "label": "MOSI\nData", "channel": "MOSI"})
678
-
679
901
  if miso_trace is not None:
680
902
  rows.append({"type": "waveform", "trace": miso_trace, "label": "MISO"})
681
903
  if show_miso:
682
904
  rows.append({"type": "packets", "label": "MISO\nData", "channel": "MISO"})
683
905
 
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
- )
906
+ return rows
694
907
 
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
908
 
703
- # Auto-calculate figure size
909
+ def _create_spi_figure(
910
+ rows: list[dict[str, Any]],
911
+ figsize: tuple[float, float] | None,
912
+ ) -> tuple[Figure, list[Axes]]:
913
+ """Create matplotlib figure and axes for SPI plot.
914
+
915
+ Args:
916
+ rows: Row specifications from _build_spi_row_list.
917
+ figsize: Figure size or None for auto-calculation.
918
+
919
+ Returns:
920
+ Tuple of (figure, axes_list).
921
+ """
922
+ height_ratios = [1.0 if row["type"] == "waveform" else 0.6 for row in rows]
923
+ n_rows = len(rows)
924
+
704
925
  if figsize is None:
705
926
  width = 14
706
927
  height = max(4, sum(height_ratios) * 1.2 + 1)
@@ -713,13 +934,26 @@ def _plot_multi_channel_spi(
713
934
  sharex=True,
714
935
  gridspec_kw={"hspace": 0.1, "height_ratios": height_ratios},
715
936
  )
716
-
717
937
  if n_rows == 1:
718
938
  axes = [axes]
719
939
 
720
- # Separate packets by channel (MOSI vs MISO)
940
+ return fig, axes
941
+
942
+
943
+ def _separate_spi_packets(
944
+ packets: list[ProtocolPacket],
945
+ ) -> tuple[list[ProtocolPacket], list[ProtocolPacket]]:
946
+ """Separate packets by channel (MOSI vs MISO).
947
+
948
+ Args:
949
+ packets: List of SPI packets.
950
+
951
+ Returns:
952
+ Tuple of (mosi_packets, miso_packets).
953
+ """
721
954
  mosi_packets = []
722
955
  miso_packets = []
956
+
723
957
  for packet in packets:
724
958
  channel = getattr(packet, "channel", None)
725
959
  if channel is None and hasattr(packet, "metadata"):
@@ -728,41 +962,121 @@ def _plot_multi_channel_spi(
728
962
  if channel == "MISO":
729
963
  miso_packets.append(packet)
730
964
  else:
731
- # Default to MOSI
732
965
  mosi_packets.append(packet)
733
966
 
734
- # If no channel info, use all packets for MOSI
735
967
  if not mosi_packets and not miso_packets:
736
968
  mosi_packets = packets
737
969
 
738
- # Plot each row
970
+ return mosi_packets, miso_packets
971
+
972
+
973
+ def _render_spi_rows(
974
+ axes: list[Axes],
975
+ rows: list[dict[str, Any]],
976
+ t_min: float,
977
+ t_max: float,
978
+ time_mult: float,
979
+ mosi_packets: list[ProtocolPacket],
980
+ miso_packets: list[ProtocolPacket],
981
+ ) -> None:
982
+ """Render all SPI plot rows (waveforms and packet data).
983
+
984
+ Args:
985
+ axes: List of matplotlib axes.
986
+ rows: Row specifications.
987
+ t_min: Minimum time value.
988
+ t_max: Maximum time value.
989
+ time_mult: Time multiplier for unit conversion.
990
+ mosi_packets: MOSI channel packets.
991
+ miso_packets: MISO channel packets.
992
+ """
739
993
  for ax, row in zip(axes, rows, strict=False):
740
994
  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=":")
995
+ _render_spi_waveform_row(ax, row, t_min, t_max, time_mult)
750
996
  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)
997
+ _render_spi_packet_row(ax, row, t_min, t_max, time_mult, mosi_packets, miso_packets)
998
+
756
999
 
757
- # Set x-axis label
1000
+ def _render_spi_waveform_row(
1001
+ ax: Axes,
1002
+ row: dict[str, Any],
1003
+ t_min: float,
1004
+ t_max: float,
1005
+ time_mult: float,
1006
+ ) -> None:
1007
+ """Render a single waveform row for SPI plot.
1008
+
1009
+ Args:
1010
+ ax: Matplotlib axis to render on.
1011
+ row: Row specification with trace and label.
1012
+ t_min: Minimum time value.
1013
+ t_max: Maximum time value.
1014
+ time_mult: Time multiplier for unit conversion.
1015
+ """
1016
+ trace = row["trace"]
1017
+ trace_time = trace.time_vector * time_mult
1018
+ trace_data = trace.data.astype(float)
1019
+ mask = (trace_time >= t_min * time_mult) & (trace_time <= t_max * time_mult)
1020
+ _plot_digital_waveform(ax, trace_time[mask], trace_data[mask])
1021
+ ax.set_ylabel(row["label"], rotation=0, ha="right", va="center", fontsize=10)
1022
+ ax.set_ylim(-0.2, 1.3)
1023
+ ax.set_yticks([])
1024
+ ax.grid(True, axis="x", alpha=0.3, linestyle=":")
1025
+
1026
+
1027
+ def _render_spi_packet_row(
1028
+ ax: Axes,
1029
+ row: dict[str, Any],
1030
+ t_min: float,
1031
+ t_max: float,
1032
+ time_mult: float,
1033
+ mosi_packets: list[ProtocolPacket],
1034
+ miso_packets: list[ProtocolPacket],
1035
+ ) -> None:
1036
+ """Render a single packet data row for SPI plot.
1037
+
1038
+ Args:
1039
+ ax: Matplotlib axis to render on.
1040
+ row: Row specification with channel and label.
1041
+ t_min: Minimum time value.
1042
+ t_max: Maximum time value.
1043
+ time_mult: Time multiplier for unit conversion.
1044
+ mosi_packets: MOSI channel packets.
1045
+ miso_packets: MISO channel packets.
1046
+ """
1047
+ channel = row.get("channel", "MOSI")
1048
+ pkts = mosi_packets if channel == "MOSI" else miso_packets
1049
+ _plot_packet_row(ax, pkts, t_min, t_max, time_mult, show_errors=True)
1050
+ ax.set_ylabel(row["label"], rotation=0, ha="right", va="center", fontsize=9)
1051
+
1052
+
1053
+ def _finalize_spi_plot(
1054
+ axes: list[Axes],
1055
+ t_min: float,
1056
+ t_max: float,
1057
+ time_mult: float,
1058
+ time_unit: str,
1059
+ title: str | None,
1060
+ ) -> None:
1061
+ """Add final formatting to SPI plot.
1062
+
1063
+ Args:
1064
+ axes: List of matplotlib axes.
1065
+ t_min: Minimum time value.
1066
+ t_max: Maximum time value.
1067
+ time_mult: Time multiplier for unit conversion.
1068
+ time_unit: Time unit string for label.
1069
+ title: Plot title or None.
1070
+ """
758
1071
  axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
759
1072
  axes[-1].set_xlim(t_min * time_mult, t_max * time_mult)
760
1073
 
761
- if title:
1074
+ fig = axes[0].get_figure()
1075
+ if title and fig is not None:
762
1076
  fig.suptitle(title, fontsize=14, y=0.98)
763
1077
 
764
- fig.tight_layout()
765
- return fig
1078
+ if fig is not None and hasattr(fig, "tight_layout"):
1079
+ fig.tight_layout()
766
1080
 
767
1081
 
768
1082
  def plot_i2c_decode(