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
@@ -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