oscura 0.5.0__py3-none-any.whl → 0.6.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 (513) 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/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,137 @@
1
+ """GPIB (IEEE-488) protocol decoder.
2
+
3
+ IEEE 488-1978 General Purpose Interface Bus (GPIB) decoder for instrument control.
4
+
5
+ References:
6
+ - IEEE 488.1-2003: IEEE Standard Digital Interface for Programmable Instrumentation
7
+ - HP Application Note 1298: Understanding GPIB Addressing
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from enum import Enum
14
+ from typing import TYPE_CHECKING
15
+
16
+ import numpy as np
17
+
18
+ if TYPE_CHECKING:
19
+ from numpy.typing import NDArray
20
+
21
+
22
+ class GPIBMessageType(Enum):
23
+ """GPIB message types."""
24
+
25
+ TALK_ADDRESS = "talk_address"
26
+ LISTEN_ADDRESS = "listen_address"
27
+ DATA = "data"
28
+ COMMAND = "command"
29
+
30
+
31
+ @dataclass
32
+ class GPIBFrame:
33
+ """Decoded GPIB frame.
34
+
35
+ Attributes:
36
+ timestamp: Frame timestamp in seconds
37
+ data: Data byte value
38
+ message_type: Type of GPIB message
39
+ description: Human-readable description
40
+ """
41
+
42
+ timestamp: float
43
+ data: int
44
+ message_type: GPIBMessageType
45
+ description: str
46
+
47
+
48
+ def decode_gpib(
49
+ dio_lines: list[NDArray[np.bool_]],
50
+ dav: NDArray[np.bool_],
51
+ nrfd: NDArray[np.bool_],
52
+ ndac: NDArray[np.bool_],
53
+ eoi: NDArray[np.bool_],
54
+ atn: NDArray[np.bool_],
55
+ sample_rate: float,
56
+ ) -> list[GPIBFrame]:
57
+ """Decode GPIB (IEEE-488) protocol.
58
+
59
+ Args:
60
+ dio_lines: List of 8 DIO line arrays (data lines)
61
+ dav: Data Valid signal (active low)
62
+ nrfd: Not Ready For Data (active low)
63
+ ndac: Not Data Accepted (active low)
64
+ eoi: End or Identify (active low)
65
+ atn: Attention (active low for command, high for data)
66
+ sample_rate: Sample rate in Hz
67
+
68
+ Returns:
69
+ List of decoded GPIB frames
70
+
71
+ Example:
72
+ >>> frames = decode_gpib(dio_lines, dav, nrfd, ndac, eoi, atn, 1e6)
73
+ >>> for frame in frames:
74
+ ... print(f"{frame.timestamp*1e6:.1f}µs: {frame.description}")
75
+ """
76
+ frames: list[GPIBFrame] = []
77
+
78
+ # Find DAV falling edges (data valid transitions)
79
+ dav_falling = np.where((dav[:-1] == True) & (dav[1:] == False))[0] # noqa: E712
80
+
81
+ for edge_idx in dav_falling:
82
+ # Sample data at DAV falling edge
83
+ data_byte = 0
84
+ for bit_idx, dio_line in enumerate(dio_lines):
85
+ if edge_idx < len(dio_line) and dio_line[edge_idx]:
86
+ data_byte |= 1 << bit_idx
87
+
88
+ timestamp = edge_idx / sample_rate
89
+
90
+ # Determine message type from ATN and EOI
91
+ atn_active = not atn[edge_idx] if edge_idx < len(atn) else False
92
+ eoi_active = not eoi[edge_idx] if edge_idx < len(eoi) else False
93
+
94
+ if atn_active:
95
+ # Command/address mode
96
+ if data_byte & 0x40: # Talk address
97
+ address = data_byte & 0x1F
98
+ frame = GPIBFrame(
99
+ timestamp=timestamp,
100
+ data=data_byte,
101
+ message_type=GPIBMessageType.TALK_ADDRESS,
102
+ description=f"Talk address {address}",
103
+ )
104
+ elif data_byte & 0x20: # Listen address
105
+ address = data_byte & 0x1F
106
+ frame = GPIBFrame(
107
+ timestamp=timestamp,
108
+ data=data_byte,
109
+ message_type=GPIBMessageType.LISTEN_ADDRESS,
110
+ description=f"Listen address {address}",
111
+ )
112
+ else: # Command
113
+ frame = GPIBFrame(
114
+ timestamp=timestamp,
115
+ data=data_byte,
116
+ message_type=GPIBMessageType.COMMAND,
117
+ description=f"Command 0x{data_byte:02X}",
118
+ )
119
+ else:
120
+ # Data mode
121
+ char = chr(data_byte) if 32 <= data_byte < 127 else ""
122
+ desc = f"Data 0x{data_byte:02X}"
123
+ if char:
124
+ desc += f" ('{char}')"
125
+ if eoi_active:
126
+ desc += " [EOI]"
127
+
128
+ frame = GPIBFrame(
129
+ timestamp=timestamp,
130
+ data=data_byte,
131
+ message_type=GPIBMessageType.DATA,
132
+ description=desc,
133
+ )
134
+
135
+ frames.append(frame)
136
+
137
+ return frames
@@ -57,17 +57,17 @@ class SPIDecoder(SyncDecoder):
57
57
  longname = "Serial Peripheral Interface"
58
58
  desc = "SPI bus protocol decoder"
59
59
 
60
- channels = [ # noqa: RUF012
60
+ channels = [
61
61
  ChannelDef("clk", "CLK", "Clock signal", required=True),
62
62
  ChannelDef("mosi", "MOSI", "Master Out Slave In", required=True),
63
63
  ]
64
64
 
65
- optional_channels = [ # noqa: RUF012
65
+ optional_channels = [
66
66
  ChannelDef("miso", "MISO", "Master In Slave Out", required=False),
67
67
  ChannelDef("cs", "CS#", "Chip Select (active low)", required=False),
68
68
  ]
69
69
 
70
- options = [ # noqa: RUF012
70
+ options = [
71
71
  OptionDef("cpol", "Clock Polarity", "Clock idle state", default=0, values=[0, 1]),
72
72
  OptionDef("cpha", "Clock Phase", "Sample edge", default=0, values=[0, 1]),
73
73
  OptionDef(
@@ -87,7 +87,7 @@ class SPIDecoder(SyncDecoder):
87
87
  ),
88
88
  ]
89
89
 
90
- annotations = [ # noqa: RUF012
90
+ annotations = [
91
91
  ("bit", "Bit value"),
92
92
  ("byte", "Decoded byte"),
93
93
  ("word", "Decoded word"),
@@ -152,12 +152,78 @@ class SPIDecoder(SyncDecoder):
152
152
  >>> for pkt in decoder.decode(clk=clk, mosi=mosi, miso=miso, sample_rate=1e9):
153
153
  ... print(f"Word: 0x{pkt.annotations['mosi_value']:04X}")
154
154
  """
155
+ clk, mosi, miso, cs, sample_rate = self._prepare_spi_signals(
156
+ trace, clk, mosi, miso, cs, sample_rate
157
+ )
158
+
159
+ if clk is None or mosi is None:
160
+ return
161
+
162
+ edges = self._find_sample_edges(clk)
163
+ if len(edges) == 0:
164
+ return
165
+
166
+ mosi_bits: list[int] = []
167
+ miso_bits: list[int] = []
168
+ word_start_idx = edges[0]
169
+ word_num = 0
170
+
171
+ for edge_idx in edges:
172
+ if not self._is_cs_active(cs, edge_idx):
173
+ mosi_bits = []
174
+ miso_bits = []
175
+ continue
176
+
177
+ mosi_bits, miso_bits = self._sample_data_lines(
178
+ mosi, miso, edge_idx, mosi_bits, miso_bits
179
+ )
180
+
181
+ if len(mosi_bits) >= self._word_size:
182
+ packet = self._build_spi_word_packet(
183
+ mosi_bits, miso_bits, word_start_idx, edge_idx, sample_rate, word_num
184
+ )
185
+
186
+ yield packet
187
+
188
+ mosi_bits = mosi_bits[self._word_size :]
189
+ miso_bits = miso_bits[self._word_size :] if miso_bits else []
190
+ word_start_idx = edge_idx
191
+ word_num += 1
192
+
193
+ def _prepare_spi_signals(
194
+ self,
195
+ trace: DigitalTrace | None,
196
+ clk: NDArray[np.bool_] | None,
197
+ mosi: NDArray[np.bool_] | None,
198
+ miso: NDArray[np.bool_] | None,
199
+ cs: NDArray[np.bool_] | None,
200
+ sample_rate: float,
201
+ ) -> tuple[
202
+ NDArray[np.bool_] | None,
203
+ NDArray[np.bool_] | None,
204
+ NDArray[np.bool_] | None,
205
+ NDArray[np.bool_] | None,
206
+ float,
207
+ ]:
208
+ """Prepare and align all SPI signals.
209
+
210
+ Args:
211
+ trace: Optional trace.
212
+ clk: Clock signal.
213
+ mosi: MOSI signal.
214
+ miso: MISO signal.
215
+ cs: CS signal.
216
+ sample_rate: Sample rate.
217
+
218
+ Returns:
219
+ Tuple of aligned signals and sample rate.
220
+ """
155
221
  if trace is not None:
156
222
  clk = trace.data
157
223
  sample_rate = trace.metadata.sample_rate
158
224
 
159
225
  if clk is None or mosi is None:
160
- return
226
+ return None, None, None, None, sample_rate
161
227
 
162
228
  n_samples = min(len(clk), len(mosi))
163
229
  if miso is not None:
@@ -172,107 +238,138 @@ class SPIDecoder(SyncDecoder):
172
238
  if cs is not None:
173
239
  cs = cs[:n_samples]
174
240
 
175
- # Determine sampling edge based on CPOL and CPHA
176
- # CPOL=0: idle low, first edge is rising
177
- # CPOL=1: idle high, first edge is falling
178
- # CPHA=0: sample on first edge
179
- # CPHA=1: sample on second edge
241
+ return clk, mosi, miso, cs, sample_rate
242
+
243
+ def _find_sample_edges(self, clk: NDArray[np.bool_]) -> NDArray[np.intp]:
244
+ """Find clock edges for sampling based on CPOL/CPHA.
245
+
246
+ Args:
247
+ clk: Clock signal.
248
+
249
+ Returns:
250
+ Array of edge indices.
251
+ """
252
+ sample_edge = self._determine_sample_edge()
253
+
254
+ if sample_edge == "rising":
255
+ return np.where(~clk[:-1] & clk[1:])[0] + 1
256
+ else:
257
+ return np.where(clk[:-1] & ~clk[1:])[0] + 1
258
+
259
+ def _determine_sample_edge(self) -> str:
260
+ """Determine which clock edge to sample on.
261
+
262
+ Returns:
263
+ "rising" or "falling".
264
+ """
180
265
  if self._cpol == 0:
181
- sample_edge = "rising" if self._cpha == 0 else "falling"
266
+ return "rising" if self._cpha == 0 else "falling"
182
267
  elif self._cpha == 0:
183
- sample_edge = "falling"
268
+ return "falling"
184
269
  else:
185
- sample_edge = "rising"
270
+ return "rising"
186
271
 
187
- # Find clock edges
188
- if sample_edge == "rising":
189
- edges = np.where(~clk[:-1] & clk[1:])[0] + 1
190
- else:
191
- edges = np.where(clk[:-1] & ~clk[1:])[0] + 1
272
+ def _is_cs_active(self, cs: NDArray[np.bool_] | None, idx: int) -> bool:
273
+ """Check if chip select is active.
192
274
 
193
- if len(edges) == 0:
194
- return
275
+ Args:
276
+ cs: CS signal or None.
277
+ idx: Index to check.
195
278
 
196
- # Collect bits into words
197
- mosi_bits: list[int] = []
198
- miso_bits: list[int] = []
199
- word_start_idx = edges[0]
200
- word_num = 0
279
+ Returns:
280
+ True if CS active or not provided.
281
+ """
282
+ if cs is None:
283
+ return True
284
+ return bool(cs[idx] == (self._cs_polarity == 1))
201
285
 
202
- for edge_idx in edges:
203
- # Check if CS is active (if provided)
204
- if cs is not None:
205
- cs_active = cs[edge_idx] == (self._cs_polarity == 1)
206
- if not cs_active:
207
- # CS not active, reset and skip
208
- if mosi_bits:
209
- # Emit partial word if any
210
- pass
211
- mosi_bits = []
212
- miso_bits = []
213
- continue
214
-
215
- # Sample MOSI
216
- mosi_bit = 1 if mosi[edge_idx] else 0
217
- mosi_bits.append(mosi_bit)
218
-
219
- # Sample MISO if available
220
- if miso is not None:
221
- miso_bit = 1 if miso[edge_idx] else 0
222
- miso_bits.append(miso_bit)
223
-
224
- # Check if we have a complete word
225
- if len(mosi_bits) >= self._word_size:
226
- # Convert bits to value
227
- mosi_value = self._bits_to_value(mosi_bits[: self._word_size])
228
- miso_value = (
229
- self._bits_to_value(miso_bits[: self._word_size]) if miso_bits else None
230
- )
286
+ def _sample_data_lines(
287
+ self,
288
+ mosi: NDArray[np.bool_],
289
+ miso: NDArray[np.bool_] | None,
290
+ idx: int,
291
+ mosi_bits: list[int],
292
+ miso_bits: list[int],
293
+ ) -> tuple[list[int], list[int]]:
294
+ """Sample MOSI and MISO lines at edge.
231
295
 
232
- # Calculate timing
233
- start_time = word_start_idx / sample_rate
234
- end_time = edge_idx / sample_rate
235
-
236
- # Encode as bytes
237
- byte_count = (self._word_size + 7) // 8
238
- mosi_bytes = mosi_value.to_bytes(byte_count, "big")
239
-
240
- # Add annotations
241
- self.put_annotation(
242
- start_time,
243
- end_time,
244
- AnnotationLevel.WORDS,
245
- f"MOSI: 0x{mosi_value:0{byte_count * 2}X}",
246
- data=mosi_bytes,
247
- )
296
+ Args:
297
+ mosi: MOSI signal.
298
+ miso: MISO signal or None.
299
+ idx: Edge index.
300
+ mosi_bits: Current MOSI bit buffer.
301
+ miso_bits: Current MISO bit buffer.
248
302
 
249
- annotations = {
250
- "word_num": word_num,
251
- "mosi_bits": mosi_bits[: self._word_size],
252
- "mosi_value": mosi_value,
253
- "word_size": self._word_size,
254
- "mode": self._cpol * 2 + self._cpha,
255
- }
256
-
257
- if miso_value is not None:
258
- annotations["miso_bits"] = miso_bits[: self._word_size]
259
- annotations["miso_value"] = miso_value
260
-
261
- packet = ProtocolPacket(
262
- timestamp=start_time,
263
- protocol="spi",
264
- data=mosi_bytes,
265
- annotations=annotations,
266
- errors=[],
267
- )
303
+ Returns:
304
+ Updated (mosi_bits, miso_bits).
305
+ """
306
+ mosi_bit = 1 if mosi[idx] else 0
307
+ mosi_bits.append(mosi_bit)
268
308
 
269
- yield packet
309
+ if miso is not None:
310
+ miso_bit = 1 if miso[idx] else 0
311
+ miso_bits.append(miso_bit)
270
312
 
271
- # Reset for next word
272
- mosi_bits = mosi_bits[self._word_size :]
273
- miso_bits = miso_bits[self._word_size :] if miso_bits else []
274
- word_start_idx = edge_idx
275
- word_num += 1
313
+ return mosi_bits, miso_bits
314
+
315
+ def _build_spi_word_packet(
316
+ self,
317
+ mosi_bits: list[int],
318
+ miso_bits: list[int],
319
+ start_idx: int,
320
+ end_idx: int,
321
+ sample_rate: float,
322
+ word_num: int,
323
+ ) -> ProtocolPacket:
324
+ """Build SPI word packet.
325
+
326
+ Args:
327
+ mosi_bits: MOSI bit buffer.
328
+ miso_bits: MISO bit buffer.
329
+ start_idx: Word start index.
330
+ end_idx: Word end index.
331
+ sample_rate: Sample rate.
332
+ word_num: Word number.
333
+
334
+ Returns:
335
+ Protocol packet.
336
+ """
337
+ mosi_value = self._bits_to_value(mosi_bits[: self._word_size])
338
+ miso_value = self._bits_to_value(miso_bits[: self._word_size]) if miso_bits else None
339
+
340
+ start_time = start_idx / sample_rate
341
+ end_time = end_idx / sample_rate
342
+
343
+ byte_count = (self._word_size + 7) // 8
344
+ mosi_bytes = mosi_value.to_bytes(byte_count, "big")
345
+
346
+ self.put_annotation(
347
+ start_time,
348
+ end_time,
349
+ AnnotationLevel.WORDS,
350
+ f"MOSI: 0x{mosi_value:0{byte_count * 2}X}",
351
+ data=mosi_bytes,
352
+ )
353
+
354
+ annotations = {
355
+ "word_num": word_num,
356
+ "mosi_bits": mosi_bits[: self._word_size],
357
+ "mosi_value": mosi_value,
358
+ "word_size": self._word_size,
359
+ "mode": self._cpol * 2 + self._cpha,
360
+ }
361
+
362
+ if miso_value is not None:
363
+ annotations["miso_bits"] = miso_bits[: self._word_size]
364
+ annotations["miso_value"] = miso_value
365
+
366
+ return ProtocolPacket(
367
+ timestamp=start_time,
368
+ protocol="spi",
369
+ data=mosi_bytes,
370
+ annotations=annotations,
371
+ errors=[],
372
+ )
276
373
 
277
374
  def _bits_to_value(self, bits: list[int]) -> int:
278
375
  """Convert bit list to integer value.