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
@@ -64,16 +64,16 @@ class SWDDecoder(SyncDecoder):
64
64
  longname = "Serial Wire Debug"
65
65
  desc = "ARM Serial Wire Debug protocol decoder"
66
66
 
67
- channels = [ # noqa: RUF012
67
+ channels = [
68
68
  ChannelDef("swclk", "SWCLK", "Serial Wire Clock", required=True),
69
69
  ChannelDef("swdio", "SWDIO", "Serial Wire Data I/O", required=True),
70
70
  ]
71
71
 
72
- optional_channels = [] # noqa: RUF012
72
+ optional_channels = []
73
73
 
74
- options = [] # noqa: RUF012
74
+ options = []
75
75
 
76
- annotations = [ # noqa: RUF012
76
+ annotations = [
77
77
  ("request", "Request packet"),
78
78
  ("ack", "ACK response"),
79
79
  ("data", "Data phase"),
@@ -85,6 +85,164 @@ class SWDDecoder(SyncDecoder):
85
85
  """Initialize SWD decoder."""
86
86
  super().__init__()
87
87
 
88
+ def _is_line_reset_sequence(
89
+ self,
90
+ swdio: NDArray[np.bool_],
91
+ start_idx: int,
92
+ edge_idx: int,
93
+ rising_edges: NDArray[np.intp],
94
+ ) -> bool:
95
+ """Check if current start bit is part of a line reset sequence.
96
+
97
+ Args:
98
+ swdio: SWDIO signal data.
99
+ start_idx: Index of potential start bit.
100
+ edge_idx: Current edge index in rising_edges array.
101
+ rising_edges: Array of rising edge indices.
102
+
103
+ Returns:
104
+ True if this appears to be a line reset sequence.
105
+ """
106
+ if start_idx == 0 or edge_idx == 0:
107
+ return False
108
+
109
+ prev_edge_idx = rising_edges[edge_idx - 1]
110
+ swdio_between = swdio[prev_edge_idx:start_idx]
111
+
112
+ # If SWDIO stayed high between edges, likely line reset
113
+ return bool(len(swdio_between) > 0 and np.all(swdio_between))
114
+
115
+ def _parse_swd_request(
116
+ self, swdio: NDArray[np.bool_], edge_idx: int, rising_edges: NDArray[np.intp]
117
+ ) -> tuple[dict[str, int], list[str], int] | None:
118
+ """Parse SWD request packet (8 bits).
119
+
120
+ Args:
121
+ swdio: SWDIO signal data.
122
+ edge_idx: Starting edge index.
123
+ rising_edges: Array of rising edge indices.
124
+
125
+ Returns:
126
+ Tuple of (fields dict, errors list, register_addr) or None if insufficient edges.
127
+ """
128
+ if edge_idx + 8 > len(rising_edges):
129
+ return None
130
+
131
+ request_bits = []
132
+ for i in range(8):
133
+ bit_idx = rising_edges[edge_idx + i]
134
+ request_bits.append(1 if swdio[bit_idx] else 0)
135
+
136
+ # Extract fields
137
+ fields = {
138
+ "start_bit": request_bits[0],
139
+ "apndp": request_bits[1],
140
+ "rnw": request_bits[2],
141
+ "addr_2": request_bits[3],
142
+ "addr_3": request_bits[4],
143
+ "parity": request_bits[5],
144
+ "stop_bit": request_bits[6],
145
+ "park_bit": request_bits[7],
146
+ }
147
+
148
+ # Validate request format
149
+ errors = []
150
+ if fields["start_bit"] != 1:
151
+ errors.append("Invalid start bit")
152
+ if fields["stop_bit"] != 0:
153
+ errors.append("Invalid stop bit")
154
+ if fields["park_bit"] != 1:
155
+ errors.append("Invalid park bit")
156
+
157
+ # Check parity (odd parity of APnDP, RnW, A[2:3])
158
+ expected_parity = (
159
+ fields["apndp"] + fields["rnw"] + fields["addr_2"] + fields["addr_3"]
160
+ ) % 2
161
+ if fields["parity"] != expected_parity:
162
+ errors.append("Request parity error")
163
+
164
+ # Construct register address
165
+ register_addr = (fields["addr_3"] << 3) | (fields["addr_2"] << 2)
166
+
167
+ return (fields, errors, register_addr)
168
+
169
+ def _parse_swd_ack(
170
+ self, swdio: NDArray[np.bool_], edge_idx: int, rising_edges: NDArray[np.intp]
171
+ ) -> tuple[int, str, list[str]] | None:
172
+ """Parse SWD ACK response (3 bits).
173
+
174
+ Args:
175
+ swdio: SWDIO signal data.
176
+ edge_idx: Starting edge index.
177
+ rising_edges: Array of rising edge indices.
178
+
179
+ Returns:
180
+ Tuple of (ack_value, ack_str, errors) or None if insufficient edges.
181
+ """
182
+ if edge_idx + 3 > len(rising_edges):
183
+ return None
184
+
185
+ ack_bits = []
186
+ for i in range(3):
187
+ bit_idx = rising_edges[edge_idx + i]
188
+ ack_bits.append(1 if swdio[bit_idx] else 0)
189
+
190
+ ack_value = (ack_bits[2] << 2) | (ack_bits[1] << 1) | ack_bits[0]
191
+
192
+ # Decode ACK
193
+ errors = []
194
+ if ack_value == SWDResponse.OK.value:
195
+ ack_str = "OK"
196
+ elif ack_value == SWDResponse.WAIT.value:
197
+ ack_str = "WAIT"
198
+ errors.append("Target responded with WAIT")
199
+ elif ack_value == SWDResponse.FAULT.value:
200
+ ack_str = "FAULT"
201
+ errors.append("Target responded with FAULT")
202
+ else:
203
+ ack_str = "INVALID"
204
+ errors.append(f"Invalid ACK: 0b{ack_value:03b}")
205
+
206
+ return (ack_value, ack_str, errors)
207
+
208
+ def _parse_swd_data(
209
+ self, swdio: NDArray[np.bool_], edge_idx: int, rising_edges: NDArray[np.intp]
210
+ ) -> tuple[int, list[str]] | None:
211
+ """Parse SWD data phase (32 bits + parity).
212
+
213
+ Args:
214
+ swdio: SWDIO signal data.
215
+ edge_idx: Starting edge index.
216
+ rising_edges: Array of rising edge indices.
217
+
218
+ Returns:
219
+ Tuple of (data_value, errors) or None if insufficient edges.
220
+ """
221
+ if edge_idx + 33 > len(rising_edges):
222
+ return None
223
+
224
+ data_bits = []
225
+ for i in range(32):
226
+ bit_idx = rising_edges[edge_idx + i]
227
+ data_bits.append(1 if swdio[bit_idx] else 0)
228
+
229
+ # Convert to value (LSB first)
230
+ data_value = 0
231
+ for i, bit in enumerate(data_bits):
232
+ data_value |= bit << i
233
+
234
+ # Parity bit
235
+ parity_idx = rising_edges[edge_idx + 32]
236
+ data_parity = 1 if swdio[parity_idx] else 0
237
+
238
+ # Check data parity (odd parity)
239
+ errors = []
240
+ expected_data_parity = sum(data_bits) % 2
241
+ if data_parity != expected_data_parity:
242
+ errors.append("Data parity error")
243
+
244
+ return (data_value, errors)
245
+
88
246
  def decode( # type: ignore[override]
89
247
  self,
90
248
  trace: DigitalTrace | None = None,
@@ -112,190 +270,186 @@ class SWDDecoder(SyncDecoder):
112
270
  if swclk is None or swdio is None:
113
271
  return
114
272
 
115
- n_samples = min(len(swclk), len(swdio))
116
- swclk = swclk[:n_samples]
117
- swdio = swdio[:n_samples]
118
-
119
- # Find rising edges of SWCLK (data sampled on rising edge)
120
- rising_edges = np.where(~swclk[:-1] & swclk[1:])[0] + 1
273
+ # Prepare signals
274
+ swclk_trimmed, swdio_trimmed = self._prepare_signals(swclk, swdio)
275
+ rising_edges = self._find_rising_edges(swclk_trimmed)
121
276
 
122
277
  if len(rising_edges) == 0:
123
278
  return
124
279
 
280
+ # Decode transactions
281
+ yield from self._decode_transactions(swdio_trimmed, rising_edges, sample_rate)
282
+
283
+ def _prepare_signals(
284
+ self,
285
+ swclk: NDArray[np.bool_],
286
+ swdio: NDArray[np.bool_],
287
+ ) -> tuple[NDArray[np.bool_], NDArray[np.bool_]]:
288
+ """Trim signals to same length."""
289
+ n_samples = min(len(swclk), len(swdio))
290
+ return swclk[:n_samples], swdio[:n_samples]
291
+
292
+ def _find_rising_edges(self, swclk: NDArray[np.bool_]) -> NDArray[np.intp]:
293
+ """Find rising edges of SWCLK."""
294
+ return np.where(~swclk[:-1] & swclk[1:])[0] + 1
295
+
296
+ def _decode_transactions(
297
+ self,
298
+ swdio: NDArray[np.bool_],
299
+ rising_edges: NDArray[np.intp],
300
+ sample_rate: float,
301
+ ) -> Iterator[ProtocolPacket]:
302
+ """Decode all SWD transactions from edge stream."""
125
303
  trans_num = 0
126
304
  edge_idx = 0
127
305
 
128
306
  while edge_idx < len(rising_edges):
129
- # Look for start bit (should be 1)
307
+ # Check for valid start bit
130
308
  start_idx = rising_edges[edge_idx]
131
309
  if not swdio[start_idx]:
132
310
  edge_idx += 1
133
311
  continue
134
312
 
135
- # Check if we have a low period before this start bit
136
- # (to avoid decoding line reset sequences as transactions)
137
- if start_idx > 0 and edge_idx > 0:
138
- prev_edge_idx = rising_edges[edge_idx - 1]
139
- # Check if there was a low period between previous and current edge
140
- # If SWDIO stayed high between edges, this might be part of line reset
141
- swdio_between = swdio[prev_edge_idx:start_idx]
142
- if len(swdio_between) > 0 and np.all(swdio_between):
143
- # SWDIO was high the entire time - likely line reset, skip
144
- edge_idx += 1
145
- continue
146
-
147
- # Parse request packet (8 bits total)
148
- # Bit 0: Start (1)
149
- # Bit 1: APnDP (0=DP, 1=AP)
150
- # Bit 2: RnW (0=Write, 1=Read)
151
- # Bits 3-4: A[2:3] (register address bits)
152
- # Bit 5: Parity (odd parity of bits 1-4)
153
- # Bit 6: Stop (0)
154
- # Bit 7: Park (1)
155
-
156
- if edge_idx + 8 > len(rising_edges):
157
- break
158
-
159
- request_bits = []
160
- for i in range(8):
161
- bit_idx = rising_edges[edge_idx + i]
162
- request_bits.append(1 if swdio[bit_idx] else 0)
163
-
164
- # Extract fields
165
- start_bit = request_bits[0]
166
- apndp = request_bits[1]
167
- rnw = request_bits[2]
168
- addr_2 = request_bits[3]
169
- addr_3 = request_bits[4]
170
- parity = request_bits[5]
171
- stop_bit = request_bits[6]
172
- park_bit = request_bits[7]
173
-
174
- # Validate request format
175
- errors = []
176
- if start_bit != 1:
177
- errors.append("Invalid start bit")
178
- if stop_bit != 0:
179
- errors.append("Invalid stop bit")
180
- if park_bit != 1:
181
- errors.append("Invalid park bit")
182
-
183
- # Check parity (odd parity of APnDP, RnW, A[2:3])
184
- expected_parity = (apndp + rnw + addr_2 + addr_3) % 2
185
- if parity != expected_parity:
186
- errors.append("Request parity error")
187
-
188
- # Construct register address
189
- register_addr = (addr_3 << 3) | (addr_2 << 2)
190
-
191
- edge_idx += 8
192
-
193
- # Turnaround period (1 clock, host releases SWDIO)
194
- edge_idx += 1
195
-
196
- # ACK response (3 bits from target)
197
- if edge_idx + 3 > len(rising_edges):
198
- break
199
-
200
- ack_bits = []
201
- for i in range(3):
202
- bit_idx = rising_edges[edge_idx + i]
203
- ack_bits.append(1 if swdio[bit_idx] else 0)
204
-
205
- ack_value = (ack_bits[2] << 2) | (ack_bits[1] << 1) | ack_bits[0]
206
-
207
- # Decode ACK
208
- if ack_value == SWDResponse.OK.value:
209
- ack_str = "OK"
210
- elif ack_value == SWDResponse.WAIT.value:
211
- ack_str = "WAIT"
212
- errors.append("Target responded with WAIT")
213
- elif ack_value == SWDResponse.FAULT.value:
214
- ack_str = "FAULT"
215
- errors.append("Target responded with FAULT")
216
- else:
217
- ack_str = "INVALID"
218
- errors.append(f"Invalid ACK: 0b{ack_value:03b}")
219
-
220
- edge_idx += 3
221
-
222
- # If ACK is OK, there's a data phase
223
- data_value = 0
224
- if ack_value == SWDResponse.OK.value:
225
- # Turnaround (1 clock)
313
+ # Skip line reset sequences
314
+ if self._is_line_reset_sequence(swdio, start_idx, edge_idx, rising_edges):
226
315
  edge_idx += 1
316
+ continue
227
317
 
228
- # Data phase (32 bits + 1 parity)
229
- if edge_idx + 33 > len(rising_edges):
230
- break
231
-
232
- data_bits = []
233
- for i in range(32):
234
- bit_idx = rising_edges[edge_idx + i]
235
- data_bits.append(1 if swdio[bit_idx] else 0)
236
-
237
- # Convert to value (LSB first)
238
- for i, bit in enumerate(data_bits):
239
- data_value |= bit << i
240
-
241
- # Parity bit
242
- parity_idx = rising_edges[edge_idx + 32]
243
- data_parity = 1 if swdio[parity_idx] else 0
244
-
245
- # Check data parity (odd parity)
246
- expected_data_parity = sum(data_bits) % 2
247
- if data_parity != expected_data_parity:
248
- errors.append("Data parity error")
249
-
250
- edge_idx += 33
251
-
252
- # Calculate timing
253
- start_time = start_idx / sample_rate
254
- end_time = rising_edges[min(edge_idx - 1, len(rising_edges) - 1)] / sample_rate
255
-
256
- # Add annotations
257
- port_type = "AP" if apndp else "DP"
258
- access_type = "Read" if rnw else "Write"
259
-
260
- self.put_annotation(
261
- start_time,
262
- end_time,
263
- AnnotationLevel.PACKETS,
264
- f"{port_type} {access_type} @ 0x{register_addr:02X}: {ack_str}",
265
- )
266
-
267
- # Create packet
268
- annotations = {
269
- "transaction_num": trans_num,
270
- "apndp": "AP" if apndp else "DP",
271
- "read": bool(rnw),
272
- "register_addr": register_addr,
273
- "ack": ack_str,
274
- "ack_value": ack_value,
275
- }
276
-
277
- if ack_value == SWDResponse.OK.value:
278
- annotations["data"] = data_value
279
-
280
- # Encode data as bytes (little-endian)
281
- data_bytes = (
282
- data_value.to_bytes(4, "little") if ack_value == SWDResponse.OK.value else b""
283
- )
284
-
285
- packet = ProtocolPacket(
286
- timestamp=start_time,
287
- protocol="swd",
288
- data=data_bytes,
289
- annotations=annotations,
290
- errors=errors,
318
+ # Parse complete transaction
319
+ packet_result = self._parse_transaction(
320
+ swdio, rising_edges, edge_idx, trans_num, sample_rate
291
321
  )
322
+ if packet_result is None:
323
+ break
292
324
 
325
+ packet, edge_advance = packet_result
293
326
  yield packet
294
327
 
295
328
  trans_num += 1
329
+ edge_idx += edge_advance
296
330
 
297
- # Turnaround and idle
298
- edge_idx += 1
331
+ def _parse_transaction(
332
+ self,
333
+ swdio: NDArray[np.bool_],
334
+ rising_edges: NDArray[np.intp],
335
+ edge_idx: int,
336
+ trans_num: int,
337
+ sample_rate: float,
338
+ ) -> tuple[ProtocolPacket, int] | None:
339
+ """Parse complete SWD transaction."""
340
+ # Parse request
341
+ request_result = self._parse_swd_request(swdio, edge_idx, rising_edges)
342
+ if request_result is None:
343
+ return None
344
+ fields, errors, register_addr = request_result
345
+
346
+ current_idx = edge_idx + 8 + 1 # Request + turnaround
347
+
348
+ # Parse ACK
349
+ ack_result = self._parse_swd_ack(swdio, current_idx, rising_edges)
350
+ if ack_result is None:
351
+ return None
352
+ ack_value, ack_str, ack_errors = ack_result
353
+ errors.extend(ack_errors)
354
+
355
+ current_idx += 3 # ACK bits
356
+
357
+ # Parse data if ACK is OK
358
+ data_value = self._parse_data_phase(swdio, rising_edges, current_idx, ack_value, errors)
359
+
360
+ if ack_value == SWDResponse.OK.value:
361
+ current_idx += 1 + 33 # Turnaround + data + parity
362
+
363
+ # Build packet
364
+ start_idx = rising_edges[edge_idx]
365
+ packet = self._build_packet(
366
+ fields,
367
+ register_addr,
368
+ ack_value,
369
+ ack_str,
370
+ data_value,
371
+ errors,
372
+ trans_num,
373
+ start_idx,
374
+ current_idx,
375
+ rising_edges,
376
+ sample_rate,
377
+ )
378
+
379
+ edge_advance = current_idx - edge_idx + 1
380
+ return packet, edge_advance
381
+
382
+ def _parse_data_phase(
383
+ self,
384
+ swdio: NDArray[np.bool_],
385
+ rising_edges: NDArray[np.intp],
386
+ current_idx: int,
387
+ ack_value: int,
388
+ errors: list[str],
389
+ ) -> int:
390
+ """Parse data phase if ACK is OK."""
391
+ if ack_value != SWDResponse.OK.value:
392
+ return 0
393
+
394
+ data_result = self._parse_swd_data(swdio, current_idx + 1, rising_edges)
395
+ if data_result is None:
396
+ return 0
397
+
398
+ data_value, data_errors = data_result
399
+ errors.extend(data_errors)
400
+ return data_value
401
+
402
+ def _build_packet(
403
+ self,
404
+ fields: dict[str, int],
405
+ register_addr: int,
406
+ ack_value: int,
407
+ ack_str: str,
408
+ data_value: int,
409
+ errors: list[str],
410
+ trans_num: int,
411
+ start_idx: int,
412
+ end_idx: int,
413
+ rising_edges: NDArray[np.intp],
414
+ sample_rate: float,
415
+ ) -> ProtocolPacket:
416
+ """Build protocol packet from parsed data."""
417
+ start_time = start_idx / sample_rate
418
+ end_time = rising_edges[min(end_idx - 1, len(rising_edges) - 1)] / sample_rate
419
+
420
+ # Add annotation
421
+ port_type = "AP" if fields["apndp"] else "DP"
422
+ access_type = "Read" if fields["rnw"] else "Write"
423
+ self.put_annotation(
424
+ start_time,
425
+ end_time,
426
+ AnnotationLevel.PACKETS,
427
+ f"{port_type} {access_type} @ 0x{register_addr:02X}: {ack_str}",
428
+ )
429
+
430
+ # Create annotations dict
431
+ annotations = {
432
+ "transaction_num": trans_num,
433
+ "apndp": "AP" if fields["apndp"] else "DP",
434
+ "read": bool(fields["rnw"]),
435
+ "register_addr": register_addr,
436
+ "ack": ack_str,
437
+ "ack_value": ack_value,
438
+ }
439
+
440
+ if ack_value == SWDResponse.OK.value:
441
+ annotations["data"] = data_value
442
+
443
+ # Encode data bytes
444
+ data_bytes = data_value.to_bytes(4, "little") if ack_value == SWDResponse.OK.value else b""
445
+
446
+ return ProtocolPacket(
447
+ timestamp=start_time,
448
+ protocol="swd",
449
+ data=data_bytes,
450
+ annotations=annotations,
451
+ errors=errors,
452
+ )
299
453
 
300
454
 
301
455
  def decode_swd(