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
@@ -66,13 +66,13 @@ class ManchesterDecoder(AsyncDecoder):
66
66
  longname = "Manchester Encoding"
67
67
  desc = "Manchester and Differential Manchester decoder"
68
68
 
69
- channels = [ # noqa: RUF012
69
+ channels = [
70
70
  ChannelDef("data", "DATA", "Manchester encoded data", required=True),
71
71
  ]
72
72
 
73
- optional_channels = [] # noqa: RUF012
73
+ optional_channels = []
74
74
 
75
- options = [ # noqa: RUF012
75
+ options = [
76
76
  OptionDef("bit_rate", "Bit rate", "Bits per second", default=10000000, values=None),
77
77
  OptionDef(
78
78
  "mode",
@@ -83,7 +83,7 @@ class ManchesterDecoder(AsyncDecoder):
83
83
  ),
84
84
  ]
85
85
 
86
- annotations = [ # noqa: RUF012
86
+ annotations = [
87
87
  ("bit", "Decoded bit"),
88
88
  ("clock", "Recovered clock"),
89
89
  ("violation", "Encoding violation"),
@@ -19,7 +19,7 @@ from __future__ import annotations
19
19
 
20
20
  from dataclasses import dataclass
21
21
  from enum import Enum
22
- from typing import TYPE_CHECKING
22
+ from typing import TYPE_CHECKING, TypedDict
23
23
 
24
24
  import numpy as np
25
25
 
@@ -42,6 +42,17 @@ if TYPE_CHECKING:
42
42
  from numpy.typing import NDArray
43
43
 
44
44
 
45
+ class _OneWireDecoderState(TypedDict):
46
+ """State dictionary for 1-Wire decoder."""
47
+
48
+ decoded_bytes: list[int]
49
+ current_bits: list[int]
50
+ rom_id: OneWireROMID | None
51
+ rom_command: int | None
52
+ errors: list[str]
53
+ transaction_start: float
54
+
55
+
45
56
  class OneWireMode(Enum):
46
57
  """1-Wire speed modes."""
47
58
 
@@ -203,13 +214,13 @@ class OneWireDecoder(AsyncDecoder):
203
214
  longname = "Dallas/Maxim 1-Wire Protocol"
204
215
  desc = "1-Wire bus decoder with ROM ID extraction"
205
216
 
206
- channels = [ # noqa: RUF012
217
+ channels = [
207
218
  ChannelDef("data", "DQ", "1-Wire data line", required=True),
208
219
  ]
209
220
 
210
- optional_channels = [] # noqa: RUF012
221
+ optional_channels = []
211
222
 
212
- options = [ # noqa: RUF012
223
+ options = [
213
224
  OptionDef(
214
225
  "mode",
215
226
  "Speed mode",
@@ -226,7 +237,7 @@ class OneWireDecoder(AsyncDecoder):
226
237
  ),
227
238
  ]
228
239
 
229
- annotations = [ # noqa: RUF012
240
+ annotations = [
230
241
  ("reset", "Reset pulse"),
231
242
  ("presence", "Presence pulse"),
232
243
  ("bit", "Data bit"),
@@ -273,172 +284,265 @@ class OneWireDecoder(AsyncDecoder):
273
284
  >>> for packet in decoder.decode(trace):
274
285
  ... print(f"Command: {packet.annotations.get('rom_command')}")
275
286
  """
276
- # Convert to digital if needed
287
+ digital_trace = self._convert_to_digital(trace)
288
+ data, sample_rate = self._extract_trace_data(digital_trace)
289
+ us_to_samples = sample_rate / 1_000_000
290
+
291
+ falling, rising = self._find_edges(data)
292
+ if len(falling) == 0:
293
+ return
294
+
295
+ state = self._init_decoder_state(falling[0] / sample_rate)
296
+
297
+ for i in range(len(falling)):
298
+ result = self._process_edge(falling, rising, i, sample_rate, us_to_samples, state)
299
+
300
+ if result is not None: # Reset pulse yielded packet
301
+ yield result
302
+
303
+ # Yield final transaction if any
304
+ if state["decoded_bytes"]:
305
+ yield self._create_packet(state)
306
+
307
+ def _convert_to_digital(self, trace: DigitalTrace | WaveformTrace) -> DigitalTrace:
308
+ """Convert trace to digital if needed."""
277
309
  if isinstance(trace, WaveformTrace):
278
310
  from oscura.analyzers.digital.extraction import to_digital
279
311
 
280
312
  threshold = self._threshold if self._threshold != "auto" else "auto"
281
- digital_trace = to_digital(trace, threshold=threshold) # type: ignore[arg-type]
282
- else:
283
- digital_trace = trace
313
+ return to_digital(trace, threshold=threshold) # type: ignore[arg-type]
314
+ return trace
284
315
 
316
+ def _extract_trace_data(self, digital_trace: DigitalTrace) -> tuple[NDArray[np.bool_], float]:
317
+ """Extract data and sample rate from digital trace."""
285
318
  data = digital_trace.data.astype(bool)
286
319
  sample_rate = digital_trace.metadata.sample_rate
320
+ return data, sample_rate
287
321
 
288
- # Convert timing specs to samples
289
- us_to_samples = sample_rate / 1_000_000
290
-
291
- # Find all falling and rising edges
322
+ def _find_edges(self, data: NDArray[np.bool_]) -> tuple[NDArray[np.intp], NDArray[np.intp]]:
323
+ """Find falling and rising edges in digital data."""
292
324
  falling = np.where((data[:-1]) & (~data[1:]))[0]
293
325
  rising = np.where((~data[:-1]) & (data[1:]))[0]
326
+ return falling, rising
327
+
328
+ def _init_decoder_state(self, start_time: float) -> _OneWireDecoderState:
329
+ """Initialize decoder state machine."""
330
+ state: _OneWireDecoderState = {
331
+ "decoded_bytes": [],
332
+ "current_bits": [],
333
+ "rom_id": None,
334
+ "rom_command": None,
335
+ "errors": [],
336
+ "transaction_start": start_time,
337
+ }
338
+ return state
294
339
 
295
- if len(falling) == 0:
340
+ def _process_edge(
341
+ self,
342
+ falling: NDArray[np.intp],
343
+ rising: NDArray[np.intp],
344
+ i: int,
345
+ sample_rate: float,
346
+ us_to_samples: float,
347
+ state: _OneWireDecoderState,
348
+ ) -> ProtocolPacket | None:
349
+ """Process single edge, return packet if reset pulse detected."""
350
+ fall_idx = falling[i]
351
+ fall_time = fall_idx / sample_rate
352
+
353
+ rise_idx = self._find_rising_edge(rising, fall_idx)
354
+ if rise_idx is None:
355
+ return None
356
+
357
+ low_duration_us = (rise_idx - fall_idx) / us_to_samples
358
+
359
+ if self._is_reset_pulse(low_duration_us):
360
+ return self._handle_reset_pulse(
361
+ state, fall_time, rise_idx, sample_rate, falling, rising, us_to_samples
362
+ )
363
+
364
+ self._handle_data_bit(low_duration_us, fall_time, rise_idx, sample_rate, state)
365
+ return None
366
+
367
+ def _find_rising_edge(self, rising: NDArray[np.intp], fall_idx: int) -> int | None:
368
+ """Find rising edge corresponding to falling edge."""
369
+ rise_candidates = rising[rising > fall_idx]
370
+ if len(rise_candidates) == 0:
371
+ return None
372
+ return int(rise_candidates[0])
373
+
374
+ def _is_reset_pulse(self, low_duration_us: float) -> bool:
375
+ """Check if pulse duration indicates reset."""
376
+ return low_duration_us >= self._timings.reset_min * 0.8
377
+
378
+ def _handle_reset_pulse(
379
+ self,
380
+ state: _OneWireDecoderState,
381
+ fall_time: float,
382
+ rise_idx: int,
383
+ sample_rate: float,
384
+ falling: NDArray[np.intp],
385
+ rising: NDArray[np.intp],
386
+ us_to_samples: float,
387
+ ) -> ProtocolPacket | None:
388
+ """Handle reset pulse and look for presence."""
389
+ packet = None
390
+ if state["decoded_bytes"]:
391
+ packet = self._create_packet(state)
392
+
393
+ # Reset state
394
+ state["transaction_start"] = fall_time
395
+ state["decoded_bytes"] = []
396
+ state["current_bits"] = []
397
+ state["rom_id"] = None
398
+ state["rom_command"] = None
399
+ state["errors"] = []
400
+
401
+ self.put_annotation(fall_time, rise_idx / sample_rate, AnnotationLevel.BITS, "Reset")
402
+ self._check_presence_pulse(rise_idx, sample_rate, falling, rising, us_to_samples)
403
+
404
+ return packet
405
+
406
+ def _check_presence_pulse(
407
+ self,
408
+ rise_idx: int,
409
+ sample_rate: float,
410
+ falling: NDArray[np.intp],
411
+ rising: NDArray[np.intp],
412
+ us_to_samples: float,
413
+ ) -> None:
414
+ """Check for presence pulse after reset."""
415
+ next_falls = falling[falling > rise_idx]
416
+ if len(next_falls) == 0:
296
417
  return
297
418
 
298
- # State machine for decoding
299
- decoded_bytes: list[int] = []
300
- current_bits: list[int] = []
301
- rom_id: OneWireROMID | None = None
302
- rom_command: int | None = None
303
- errors: list[str] = []
304
- transaction_start: float = falling[0] / sample_rate
305
-
306
- i = 0
307
- while i < len(falling):
308
- fall_idx = falling[i]
309
- fall_time = fall_idx / sample_rate
310
-
311
- # Find corresponding rising edge
312
- rise_candidates = rising[rising > fall_idx]
313
- if len(rise_candidates) == 0:
314
- break
315
-
316
- rise_idx = rise_candidates[0]
317
- low_duration_us = (rise_idx - fall_idx) / us_to_samples
318
-
319
- # Check for reset pulse
320
- if low_duration_us >= self._timings.reset_min * 0.8:
321
- # This is a reset pulse
322
- if decoded_bytes:
323
- # Yield previous transaction
324
- annotations = self._build_annotations(decoded_bytes, rom_command, rom_id)
325
- yield ProtocolPacket(
326
- timestamp=transaction_start,
327
- protocol="1-wire",
328
- data=bytes(decoded_bytes),
329
- annotations=annotations,
330
- errors=errors.copy() if errors else None, # type: ignore[arg-type]
331
- )
332
-
333
- # Start new transaction
334
- transaction_start = fall_time
335
- decoded_bytes = []
336
- current_bits = []
337
- rom_id = None
338
- rom_command = None
339
- errors = []
340
-
341
- self.put_annotation(
342
- fall_time,
343
- rise_idx / sample_rate,
344
- AnnotationLevel.BITS,
345
- "Reset",
346
- )
347
-
348
- # Look for presence pulse (pulled low by slave)
349
- next_falls = falling[falling > rise_idx]
350
- if len(next_falls) > 0:
351
- next_fall = next_falls[0]
352
- wait_time_us = (next_fall - rise_idx) / us_to_samples
353
- if wait_time_us < self._timings.presence_max * 2:
354
- # Found presence response
355
- next_rises = rising[rising > next_fall]
356
- if len(next_rises) > 0:
357
- presence_end = next_rises[0]
358
- presence_us = (presence_end - next_fall) / us_to_samples
359
- if (
360
- self._timings.presence_min * 0.5
361
- <= presence_us
362
- <= self._timings.presence_max * 1.5
363
- ):
364
- self.put_annotation(
365
- next_fall / sample_rate,
366
- presence_end / sample_rate,
367
- AnnotationLevel.BITS,
368
- "Presence",
369
- )
370
- i += 1
371
- continue
372
-
373
- # Data bit - determine if 0 or 1
374
- # Short low pulse = write 1 or read 1
375
- # Long low pulse = write 0 or read 0
376
- if low_duration_us < self._timings.write_1_low_max * 2:
377
- bit = 1
378
- elif low_duration_us >= self._timings.write_0_low_min * 0.5:
379
- bit = 0
380
- else:
381
- # Ambiguous timing
382
- bit = 1 if low_duration_us < self._timings.slot_min * 0.5 else 0
383
-
384
- current_bits.append(bit)
385
-
386
- # Assemble bytes (LSB first)
387
- if len(current_bits) == 8:
388
- byte_val = sum(b << i for i, b in enumerate(current_bits))
389
- decoded_bytes.append(byte_val)
390
- current_bits = []
391
-
392
- # Check for ROM command (first byte after reset)
393
- if len(decoded_bytes) == 1:
394
- rom_command = byte_val
395
- cmd_name = ROM_COMMAND_NAMES.get(byte_val, f"Unknown (0x{byte_val:02X})")
396
- self.put_annotation(
397
- fall_time,
398
- rise_idx / sample_rate,
399
- AnnotationLevel.BYTES,
400
- f"ROM Cmd: {cmd_name}",
401
- )
402
-
403
- # Check for ROM ID (8 bytes after ROM command)
404
- if len(decoded_bytes) == 9 and rom_command in (
405
- OneWireROMCommand.READ_ROM.value,
406
- OneWireROMCommand.MATCH_ROM.value,
407
- ):
408
- try:
409
- rom_id = OneWireROMID.from_bytes(bytes(decoded_bytes[1:9]))
410
- if not rom_id.verify_crc():
411
- errors.append("ROM ID CRC error")
412
- self.put_annotation(
413
- transaction_start,
414
- rise_idx / sample_rate,
415
- AnnotationLevel.BYTES,
416
- f"ROM: {rom_id.to_hex()}",
417
- )
418
- except ValueError as e:
419
- errors.append(f"ROM parse error: {e}")
420
-
421
- i += 1
419
+ next_fall = next_falls[0]
420
+ wait_time_us = (next_fall - rise_idx) / us_to_samples
421
+ if wait_time_us >= self._timings.presence_max * 2:
422
+ return
422
423
 
423
- # Yield final transaction if any
424
- if decoded_bytes:
425
- annotations = self._build_annotations(decoded_bytes, rom_command, rom_id)
426
- yield ProtocolPacket(
427
- timestamp=transaction_start,
428
- protocol="1-wire",
429
- data=bytes(decoded_bytes),
430
- annotations=annotations,
431
- errors=errors if errors else None, # type: ignore[arg-type]
424
+ next_rises = rising[rising > next_fall]
425
+ if len(next_rises) == 0:
426
+ return
427
+
428
+ presence_end = next_rises[0]
429
+ presence_us = (presence_end - next_fall) / us_to_samples
430
+ if self._timings.presence_min * 0.5 <= presence_us <= self._timings.presence_max * 1.5:
431
+ self.put_annotation(
432
+ next_fall / sample_rate,
433
+ presence_end / sample_rate,
434
+ AnnotationLevel.BITS,
435
+ "Presence",
436
+ )
437
+
438
+ def _handle_data_bit(
439
+ self,
440
+ low_duration_us: float,
441
+ fall_time: float,
442
+ rise_idx: int,
443
+ sample_rate: float,
444
+ state: _OneWireDecoderState,
445
+ ) -> None:
446
+ """Decode and process data bit."""
447
+ bit = self._decode_bit(low_duration_us)
448
+ state["current_bits"].append(bit)
449
+
450
+ if len(state["current_bits"]) == 8:
451
+ self._process_complete_byte(state, fall_time, rise_idx, sample_rate)
452
+
453
+ def _decode_bit(self, low_duration_us: float) -> int:
454
+ """Decode bit from pulse duration."""
455
+ if low_duration_us < self._timings.write_1_low_max * 2:
456
+ return 1
457
+ elif low_duration_us >= self._timings.write_0_low_min * 0.5:
458
+ return 0
459
+ else:
460
+ return 1 if low_duration_us < self._timings.slot_min * 0.5 else 0
461
+
462
+ def _process_complete_byte(
463
+ self,
464
+ state: _OneWireDecoderState,
465
+ fall_time: float,
466
+ rise_idx: int,
467
+ sample_rate: float,
468
+ ) -> None:
469
+ """Process complete 8-bit byte."""
470
+ byte_val = sum(b << i for i, b in enumerate(state["current_bits"]))
471
+ state["decoded_bytes"].append(byte_val)
472
+ state["current_bits"] = []
473
+
474
+ if len(state["decoded_bytes"]) == 1:
475
+ self._handle_rom_command(byte_val, fall_time, rise_idx, sample_rate, state)
476
+ elif len(state["decoded_bytes"]) == 9:
477
+ self._handle_rom_id(state, rise_idx, sample_rate)
478
+
479
+ def _handle_rom_command(
480
+ self,
481
+ byte_val: int,
482
+ fall_time: float,
483
+ rise_idx: int,
484
+ sample_rate: float,
485
+ state: _OneWireDecoderState,
486
+ ) -> None:
487
+ """Handle ROM command (first byte)."""
488
+ state["rom_command"] = byte_val
489
+ cmd_name = ROM_COMMAND_NAMES.get(byte_val, f"Unknown (0x{byte_val:02X})")
490
+ self.put_annotation(
491
+ fall_time,
492
+ rise_idx / sample_rate,
493
+ AnnotationLevel.BYTES,
494
+ f"ROM Cmd: {cmd_name}",
495
+ )
496
+
497
+ def _handle_rom_id(
498
+ self,
499
+ state: _OneWireDecoderState,
500
+ rise_idx: int,
501
+ sample_rate: float,
502
+ ) -> None:
503
+ """Handle ROM ID (8 bytes after ROM command)."""
504
+ rom_command = state["rom_command"]
505
+ if rom_command not in (
506
+ OneWireROMCommand.READ_ROM.value,
507
+ OneWireROMCommand.MATCH_ROM.value,
508
+ ):
509
+ return
510
+
511
+ try:
512
+ rom_id = OneWireROMID.from_bytes(bytes(state["decoded_bytes"][1:9]))
513
+ if not rom_id.verify_crc():
514
+ state["errors"].append("ROM ID CRC error")
515
+ state["rom_id"] = rom_id
516
+ self.put_annotation(
517
+ state["transaction_start"],
518
+ rise_idx / sample_rate,
519
+ AnnotationLevel.BYTES,
520
+ f"ROM: {rom_id.to_hex()}",
432
521
  )
522
+ except ValueError as e:
523
+ state["errors"].append(f"ROM parse error: {e}")
524
+
525
+ def _create_packet(self, state: _OneWireDecoderState) -> ProtocolPacket:
526
+ """Create protocol packet from decoder state."""
527
+ annotations = self._build_annotations(
528
+ state["decoded_bytes"], state["rom_command"], state["rom_id"]
529
+ )
530
+ return ProtocolPacket(
531
+ timestamp=state["transaction_start"],
532
+ protocol="1-wire",
533
+ data=bytes(state["decoded_bytes"]),
534
+ annotations=annotations,
535
+ errors=state["errors"].copy() if state["errors"] else None, # type: ignore[arg-type]
536
+ )
433
537
 
434
538
  def _build_annotations(
435
539
  self,
436
540
  decoded_bytes: list[int],
437
541
  rom_command: int | None,
438
542
  rom_id: OneWireROMID | None,
439
- ) -> dict: # type: ignore[type-arg]
543
+ ) -> dict[str, object]:
440
544
  """Build annotation dictionary for packet."""
441
- annotations: dict = { # type: ignore[type-arg]
545
+ annotations: dict[str, object] = {
442
546
  "mode": self._mode.value,
443
547
  "byte_count": len(decoded_bytes),
444
548
  }
@@ -0,0 +1,20 @@
1
+ """Parallel bus protocol decoders.
2
+
3
+ This module provides decoders for parallel bus protocols including:
4
+ - GPIB (IEEE-488): General Purpose Interface Bus for instrument control
5
+ - Centronics: Parallel printer interface
6
+ - ISA: Industry Standard Architecture bus
7
+
8
+ Example:
9
+ >>> from oscura.analyzers.protocols.parallel_bus import decode_gpib
10
+ >>> frames = decode_gpib(dio_lines, dav, nrfd, ndac, eoi, atn, sample_rate)
11
+ >>> for frame in frames:
12
+ ... print(f"Type: {frame.message_type}, Data: 0x{frame.data:02X}")
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from oscura.analyzers.protocols.parallel_bus.centronics import decode_centronics
18
+ from oscura.analyzers.protocols.parallel_bus.gpib import decode_gpib
19
+
20
+ __all__ = ["decode_centronics", "decode_gpib"]
@@ -0,0 +1,92 @@
1
+ """Centronics parallel printer protocol decoder.
2
+
3
+ Decoder for the Centronics parallel printer interface, commonly used
4
+ in legacy printer connections (pre-USB).
5
+
6
+ Protocol Overview:
7
+ - 8 data lines (D0-D7)
8
+ - STROBE: Latches data (active low)
9
+ - BUSY: Printer busy signal (active high)
10
+ - ACK: Acknowledge data received (active low)
11
+
12
+ References:
13
+ - Centronics Data Computer Corp. Parallel Interface Standard
14
+ - IEEE 1284-1994 (modern parallel port standard based on Centronics)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ from typing import TYPE_CHECKING
21
+
22
+ import numpy as np
23
+
24
+ if TYPE_CHECKING:
25
+ from numpy.typing import NDArray
26
+
27
+
28
+ @dataclass
29
+ class CentronicsFrame:
30
+ """Decoded Centronics frame.
31
+
32
+ Attributes:
33
+ timestamp: Frame timestamp in seconds
34
+ data: Data byte value
35
+ character: ASCII character if printable, None otherwise
36
+ """
37
+
38
+ timestamp: float
39
+ data: int
40
+ character: str | None
41
+
42
+
43
+ def decode_centronics(
44
+ data_lines: list[NDArray[np.bool_]],
45
+ strobe: NDArray[np.bool_],
46
+ busy: NDArray[np.bool_],
47
+ ack: NDArray[np.bool_],
48
+ sample_rate: float,
49
+ ) -> list[CentronicsFrame]:
50
+ """Decode Centronics parallel printer protocol.
51
+
52
+ Args:
53
+ data_lines: List of 8 data line arrays (D0-D7)
54
+ strobe: Strobe signal (active low, latches data)
55
+ busy: Busy signal (active high when printer busy)
56
+ ack: Acknowledge signal (active low when data accepted)
57
+ sample_rate: Sample rate in Hz
58
+
59
+ Returns:
60
+ List of decoded Centronics frames
61
+
62
+ Example:
63
+ >>> frames = decode_centronics(data_lines, strobe, busy, ack, 1e6)
64
+ >>> text = ''.join(f.character or f'\\x{f.data:02x}' for f in frames)
65
+ >>> print(f"Printer data: {text}")
66
+ """
67
+ frames: list[CentronicsFrame] = []
68
+
69
+ # Find STROBE falling edges (data latch events)
70
+ strobe_falling = np.where((strobe[:-1] == True) & (strobe[1:] == False))[0] # noqa: E712
71
+
72
+ for edge_idx in strobe_falling:
73
+ # Sample data at STROBE falling edge
74
+ data_byte = 0
75
+ for bit_idx, data_line in enumerate(data_lines):
76
+ if edge_idx < len(data_line) and data_line[edge_idx]:
77
+ data_byte |= 1 << bit_idx
78
+
79
+ timestamp = edge_idx / sample_rate
80
+
81
+ # Convert to character if printable ASCII
82
+ character = chr(data_byte) if 32 <= data_byte < 127 else None
83
+
84
+ frame = CentronicsFrame(
85
+ timestamp=timestamp,
86
+ data=data_byte,
87
+ character=character,
88
+ )
89
+
90
+ frames.append(frame)
91
+
92
+ return frames