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
@@ -0,0 +1,204 @@
1
+ """Oscura Validate Command - Protocol Specification Validation.
2
+
3
+ Provides CLI for validating protocol specifications and message structures.
4
+
5
+
6
+ Example:
7
+ $ oscura validate protocol_spec.yaml
8
+ $ oscura validate --spec uart --test-data capture.wfm
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ import click
18
+
19
+ logger = logging.getLogger("oscura.cli.validate")
20
+
21
+
22
+ @click.command()
23
+ @click.argument("spec", type=click.Path(exists=True))
24
+ @click.option(
25
+ "--test-data",
26
+ type=click.Path(exists=True),
27
+ default=None,
28
+ help="Test data file for validation.",
29
+ )
30
+ @click.option(
31
+ "--output",
32
+ type=click.Choice(["json", "table"], case_sensitive=False),
33
+ default="table",
34
+ help="Output format.",
35
+ )
36
+ @click.pass_context
37
+ def validate(
38
+ ctx: click.Context,
39
+ spec: str,
40
+ test_data: str | None,
41
+ output: str,
42
+ ) -> None:
43
+ """Validate protocol specification.
44
+
45
+ Validates protocol specification files (YAML/JSON) and optionally
46
+ tests them against real data.
47
+
48
+ Args:
49
+ ctx: Click context object.
50
+ spec: Path to specification file.
51
+ test_data: Optional test data file.
52
+ output: Output format.
53
+
54
+ Examples:
55
+
56
+ \b
57
+ # Validate spec only
58
+ $ oscura validate protocol_spec.yaml
59
+
60
+ \b
61
+ # Validate against test data
62
+ $ oscura validate uart_spec.yaml --test-data capture.wfm
63
+ """
64
+ verbose = ctx.obj.get("verbose", 0)
65
+
66
+ if verbose:
67
+ logger.info(f"Validating specification: {spec}")
68
+
69
+ try:
70
+ import yaml
71
+
72
+ # Load specification
73
+ spec_path = Path(spec)
74
+ with open(spec_path) as f:
75
+ if spec_path.suffix in [".yaml", ".yml"]:
76
+ spec_data = yaml.safe_load(f)
77
+ else:
78
+ import json
79
+
80
+ spec_data = json.load(f)
81
+
82
+ # Validate spec structure
83
+ validation_results: dict[str, Any] = {
84
+ "spec_file": str(spec_path.name),
85
+ "valid": True,
86
+ "errors": [],
87
+ "warnings": [],
88
+ }
89
+
90
+ _validate_spec_structure(spec_data, validation_results)
91
+
92
+ # Validate against test data if provided
93
+ if test_data:
94
+ from oscura.loaders import load
95
+
96
+ trace = load(test_data)
97
+ _validate_against_data(spec_data, trace, validation_results)
98
+
99
+ # Output results
100
+ if output == "json":
101
+ import json
102
+
103
+ click.echo(json.dumps(validation_results, indent=2))
104
+ else:
105
+ _print_validation_results(validation_results)
106
+
107
+ # Exit with error if validation failed
108
+ if not validation_results["valid"]:
109
+ ctx.exit(1)
110
+
111
+ except Exception as e:
112
+ logger.error(f"Validation failed: {e}")
113
+ if verbose > 1:
114
+ raise
115
+ click.echo(f"Error: {e}", err=True)
116
+ ctx.exit(1)
117
+
118
+
119
+ def _validate_spec_structure(spec: dict[str, Any], results: dict[str, Any]) -> None:
120
+ """Validate specification structure.
121
+
122
+ Args:
123
+ spec: Specification dictionary.
124
+ results: Results dictionary to update.
125
+ """
126
+ required_fields = ["name", "version"]
127
+
128
+ for field in required_fields:
129
+ if field not in spec:
130
+ results["errors"].append(f"Missing required field: {field}")
131
+ results["valid"] = False
132
+
133
+ # Check optional but recommended fields
134
+ recommended_fields = ["description", "fields", "constraints"]
135
+ for field in recommended_fields:
136
+ if field not in spec:
137
+ results["warnings"].append(f"Missing recommended field: {field}")
138
+
139
+ # Validate fields if present
140
+ if "fields" in spec and isinstance(spec["fields"], list):
141
+ for i, field in enumerate(spec["fields"]):
142
+ if not isinstance(field, dict):
143
+ results["errors"].append(f"Field {i} is not a dictionary")
144
+ results["valid"] = False
145
+ elif "name" not in field:
146
+ results["errors"].append(f"Field {i} missing 'name' attribute")
147
+ results["valid"] = False
148
+
149
+
150
+ def _validate_against_data(spec: dict[str, Any], trace: Any, results: dict[str, Any]) -> None:
151
+ """Validate specification against test data.
152
+
153
+ Args:
154
+ spec: Specification dictionary.
155
+ trace: Test data trace.
156
+ results: Results dictionary to update.
157
+ """
158
+ # Check sample rate requirements
159
+ if "sample_rate_min" in spec:
160
+ min_rate = spec["sample_rate_min"]
161
+ if trace.metadata.sample_rate < min_rate:
162
+ results["warnings"].append(
163
+ f"Sample rate {trace.metadata.sample_rate} below minimum {min_rate}"
164
+ )
165
+
166
+ # Check data length
167
+ if "min_samples" in spec:
168
+ min_samples = spec["min_samples"]
169
+ if len(trace.data) < min_samples:
170
+ results["errors"].append(
171
+ f"Data has {len(trace.data)} samples, need at least {min_samples}"
172
+ )
173
+ results["valid"] = False
174
+
175
+ results["test_data_samples"] = len(trace.data)
176
+ results["test_data_sample_rate"] = trace.metadata.sample_rate
177
+
178
+
179
+ def _print_validation_results(results: dict[str, Any]) -> None:
180
+ """Print validation results.
181
+
182
+ Args:
183
+ results: Validation results dictionary.
184
+ """
185
+ click.echo("\n=== Validation Results ===\n")
186
+ click.echo(f"Specification: {results['spec_file']}")
187
+ click.echo(f"Status: {'PASS' if results['valid'] else 'FAIL'}\n")
188
+
189
+ if results["errors"]:
190
+ click.echo("Errors:")
191
+ for err in results["errors"]:
192
+ click.echo(f" - {err}")
193
+ click.echo()
194
+
195
+ if results["warnings"]:
196
+ click.echo("Warnings:")
197
+ for warn in results["warnings"]:
198
+ click.echo(f" - {warn}")
199
+ click.echo()
200
+
201
+ if "test_data_samples" in results:
202
+ click.echo("Test Data:")
203
+ click.echo(f" Samples: {results['test_data_samples']}")
204
+ click.echo(f" Sample Rate: {results['test_data_sample_rate']} Hz")
@@ -0,0 +1,158 @@
1
+ """Oscura Visualize Command - Interactive Waveform Viewer.
2
+
3
+ Provides CLI for launching interactive waveform visualization.
4
+
5
+
6
+ Example:
7
+ $ oscura visualize signal.wfm
8
+ $ oscura visualize capture.wfm --protocol uart
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING
16
+
17
+ import click
18
+
19
+ if TYPE_CHECKING:
20
+ from matplotlib.axes import Axes
21
+
22
+ from oscura.core.types import WaveformTrace
23
+
24
+ logger = logging.getLogger("oscura.cli.visualize")
25
+
26
+
27
+ @click.command()
28
+ @click.argument("file", type=click.Path(exists=True))
29
+ @click.option(
30
+ "--protocol",
31
+ type=str,
32
+ default=None,
33
+ help="Protocol for overlay annotations.",
34
+ )
35
+ @click.option(
36
+ "--save",
37
+ type=click.Path(),
38
+ default=None,
39
+ help="Save plot to file instead of showing.",
40
+ )
41
+ @click.pass_context
42
+ def visualize(
43
+ ctx: click.Context,
44
+ file: str,
45
+ protocol: str | None,
46
+ save: str | None,
47
+ ) -> None:
48
+ """Launch interactive waveform viewer.
49
+
50
+ Opens an interactive plot window for waveform analysis with zoom,
51
+ pan, and measurement tools.
52
+
53
+ Args:
54
+ ctx: Click context object.
55
+ file: Path to waveform file.
56
+ protocol: Optional protocol for annotations.
57
+ save: Save plot to file instead of displaying.
58
+
59
+ Examples:
60
+
61
+ \b
62
+ # Interactive viewer
63
+ $ oscura visualize signal.wfm
64
+
65
+ \b
66
+ # With protocol overlay
67
+ $ oscura visualize capture.wfm --protocol uart
68
+
69
+ \b
70
+ # Save to file
71
+ $ oscura visualize data.wfm --save plot.png
72
+ """
73
+ verbose = ctx.obj.get("verbose", 0)
74
+
75
+ if verbose:
76
+ logger.info(f"Visualizing: {file}")
77
+
78
+ try:
79
+ import matplotlib.pyplot as plt
80
+ import numpy as np
81
+
82
+ from oscura.core.types import IQTrace, WaveformTrace
83
+ from oscura.loaders import load
84
+
85
+ # Load file
86
+ trace = load(file)
87
+
88
+ # Create plot
89
+ fig, ax = plt.subplots(figsize=(12, 6))
90
+
91
+ # Plot waveform (handle IQTrace separately)
92
+ if isinstance(trace, IQTrace):
93
+ # Plot I/Q components
94
+ time_axis = np.arange(len(trace.i_data)) / trace.metadata.sample_rate
95
+ ax.plot(time_axis * 1e3, trace.i_data, linewidth=0.5, label="I")
96
+ ax.plot(time_axis * 1e3, trace.q_data, linewidth=0.5, label="Q")
97
+ ax.legend()
98
+ else:
99
+ # Plot regular waveform or digital trace
100
+ time_axis = np.arange(len(trace.data)) / trace.metadata.sample_rate
101
+ ax.plot(time_axis * 1e3, trace.data, linewidth=0.5)
102
+
103
+ ax.set_xlabel("Time (ms)")
104
+ ax.set_ylabel("Voltage (V)")
105
+ ax.set_title(f"Waveform: {Path(file).name}")
106
+ ax.grid(True, alpha=0.3)
107
+
108
+ # Add protocol overlay if requested (only for non-IQ traces)
109
+ if protocol and isinstance(trace, WaveformTrace):
110
+ _add_protocol_overlay(ax, trace, protocol)
111
+
112
+ # Save or show
113
+ if save:
114
+ plt.savefig(save, dpi=300, bbox_inches="tight")
115
+ click.echo(f"Saved to: {save}")
116
+ else:
117
+ plt.tight_layout()
118
+ plt.show()
119
+
120
+ except Exception as e:
121
+ logger.error(f"Visualization failed: {e}")
122
+ if verbose > 1:
123
+ raise
124
+ click.echo(f"Error: {e}", err=True)
125
+ ctx.exit(1)
126
+
127
+
128
+ def _add_protocol_overlay(ax: Axes, trace: WaveformTrace, protocol: str) -> None:
129
+ """Add protocol annotations to plot.
130
+
131
+ Args:
132
+ ax: Matplotlib axis.
133
+ trace: Waveform trace.
134
+ protocol: Protocol name.
135
+ """
136
+ import numpy as np
137
+
138
+ # Convert to digital
139
+ threshold = (np.max(trace.data) + np.min(trace.data)) / 2
140
+ digital = trace.data > threshold
141
+
142
+ # Find edges
143
+ edges = np.where(np.diff(digital.astype(int)) != 0)[0]
144
+
145
+ # Mark edges
146
+ time_axis = np.arange(len(trace.data)) / trace.metadata.sample_rate
147
+ for edge in edges[:100]: # Limit to first 100 edges
148
+ ax.axvline(time_axis[edge] * 1e3, color="red", alpha=0.3, linewidth=0.5)
149
+
150
+ ax.text(
151
+ 0.98,
152
+ 0.98,
153
+ f"Protocol: {protocol.upper()}",
154
+ transform=ax.transAxes,
155
+ ha="right",
156
+ va="top",
157
+ bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5},
158
+ )
oscura/convenience.py CHANGED
@@ -195,109 +195,142 @@ def auto_decode(
195
195
  References:
196
196
  sigrok Protocol Decoder API
197
197
  """
198
+
199
+ # Prepare digital trace
200
+ digital_trace = _prepare_digital_trace(trace)
201
+
202
+ # Detect or use specified protocol
203
+ protocol, config, confidence = _detect_or_select_protocol(
204
+ trace, protocol, min_confidence, digital_trace
205
+ )
206
+
207
+ # Decode frames
208
+ frames, errors = _decode_protocol_frames(protocol, config, digital_trace)
209
+
210
+ # Calculate statistics
211
+ error_frames = sum(1 for f in frames if hasattr(f, "errors") and f.errors)
212
+ statistics = {
213
+ "total_frames": len(frames),
214
+ "error_frames": error_frames,
215
+ "error_rate": error_frames / len(frames) if frames else 0,
216
+ }
217
+
218
+ return DecodeResult(
219
+ protocol=protocol,
220
+ frames=frames,
221
+ confidence=confidence,
222
+ baud_rate=config.get("baud_rate"),
223
+ config=config,
224
+ errors=errors,
225
+ statistics=statistics,
226
+ )
227
+
228
+
229
+ def _prepare_digital_trace(trace: WaveformTrace | DigitalTrace) -> DigitalTrace:
230
+ """Convert to digital trace if needed."""
198
231
  from oscura.core.types import WaveformTrace
199
- from oscura.inference.protocol import detect_protocol
200
232
 
201
- # Convert to digital if needed
202
233
  if isinstance(trace, WaveformTrace):
203
234
  from oscura.analyzers.digital.extraction import to_digital
204
235
 
205
- digital_trace = to_digital(trace, threshold="auto")
206
- else:
207
- digital_trace = trace
236
+ return to_digital(trace, threshold="auto")
237
+ return trace
238
+
239
+
240
+ def _detect_or_select_protocol(
241
+ trace: WaveformTrace | DigitalTrace,
242
+ protocol: str | None,
243
+ min_confidence: float,
244
+ digital_trace: DigitalTrace,
245
+ ) -> tuple[str, dict[str, Any], float]:
246
+ """Detect protocol or use specified one."""
247
+ from oscura.core.types import WaveformTrace
208
248
 
209
- # Auto-detect protocol if not specified
210
249
  if protocol is None or protocol.lower() == "auto":
211
- # detect_protocol only accepts WaveformTrace, use original trace if it's waveform
212
250
  if isinstance(trace, WaveformTrace):
251
+ from oscura.inference.protocol import detect_protocol
252
+
213
253
  detection = detect_protocol(
214
254
  trace, min_confidence=min_confidence, return_candidates=True
215
255
  )
216
256
  else:
217
- # For DigitalTrace, we can't auto-detect protocol, use default UART
218
257
  detection = {"protocol": "UART", "config": {}, "confidence": 0.5}
219
- protocol = detection.get("protocol", "unknown")
220
- config = detection.get("config", {})
221
- confidence = detection.get("confidence", 0.0)
258
+ return (
259
+ detection.get("protocol", "unknown"),
260
+ detection.get("config", {}),
261
+ detection.get("confidence", 0.0),
262
+ )
222
263
  else:
223
- protocol = protocol.upper()
224
- confidence = 1.0
225
- config = _get_default_protocol_config(protocol)
264
+ return protocol.upper(), _get_default_protocol_config(protocol.upper()), 1.0
265
+
226
266
 
227
- # Decode based on protocol
267
+ def _decode_protocol_frames(
268
+ protocol: str, config: dict[str, Any], digital_trace: DigitalTrace
269
+ ) -> tuple[list[Any], list[str]]:
270
+ """Decode frames based on detected protocol."""
228
271
  frames: list[Any] = []
229
272
  errors: list[str] = []
230
273
 
231
274
  try:
232
275
  if protocol == "UART":
233
- from oscura.analyzers.protocols.uart import UARTDecoder
234
-
235
- baud_rate = config.get("baud_rate", 115200)
236
- decoder = UARTDecoder(
237
- baudrate=baud_rate,
238
- data_bits=config.get("data_bits", 8),
239
- parity=config.get("parity", "none"),
240
- stop_bits=config.get("stop_bits", 1),
241
- )
242
- frames = list(decoder.decode(digital_trace))
243
-
276
+ frames = _decode_uart(digital_trace, config)
244
277
  elif protocol == "SPI":
245
- from oscura.analyzers.protocols.spi import SPIDecoder
246
-
247
- spi_decoder = SPIDecoder(
248
- cpol=config.get("clock_polarity", 0),
249
- cpha=config.get("clock_phase", 0),
250
- )
251
- # Single channel decode - pass trace with channel mapping
252
- frames = list(
253
- spi_decoder.decode(digital_trace, clk=digital_trace.data, mosi=digital_trace.data)
254
- )
255
-
278
+ frames = _decode_spi(digital_trace, config)
256
279
  elif protocol == "I2C":
257
- from oscura.analyzers.protocols.i2c import I2CDecoder
258
-
259
- i2c_decoder = I2CDecoder()
260
- # Single channel - use SDA and create synthetic SCL
261
- sda = digital_trace.data
262
- edges = np.where(np.diff(sda.astype(int)) != 0)[0]
263
- scl = np.ones_like(sda, dtype=bool)
264
- for i, edge in enumerate(edges):
265
- if i % 2 == 0 and edge + 10 < len(scl):
266
- scl[edge : edge + 10] = False
267
- frames = list(i2c_decoder.decode(digital_trace, scl=scl, sda=sda))
268
-
280
+ frames = _decode_i2c(digital_trace, config)
269
281
  elif protocol == "CAN":
270
- from oscura.analyzers.protocols.can import CANDecoder
271
-
272
- can_decoder = CANDecoder(
273
- bitrate=config.get("baud_rate", 500000),
274
- sample_point=config.get("sample_point", 0.75),
275
- )
276
- frames = list(can_decoder.decode(digital_trace))
277
-
282
+ frames = _decode_can(digital_trace, config)
278
283
  else:
279
284
  errors.append(f"Unsupported protocol: {protocol}")
280
-
281
285
  except Exception as e:
282
286
  errors.append(f"Decoding error: {e!s}")
283
287
 
284
- # Calculate statistics
285
- error_frames = sum(1 for f in frames if hasattr(f, "errors") and f.errors)
286
- statistics = {
287
- "total_frames": len(frames),
288
- "error_frames": error_frames,
289
- "error_rate": error_frames / len(frames) if frames else 0,
290
- }
288
+ return frames, errors
291
289
 
292
- return DecodeResult(
293
- protocol=protocol,
294
- frames=frames,
295
- confidence=confidence,
296
- baud_rate=config.get("baud_rate"),
297
- config=config,
298
- errors=errors,
299
- statistics=statistics,
290
+
291
+ def _decode_uart(digital_trace: DigitalTrace, config: dict[str, Any]) -> list[Any]:
292
+ """Decode UART frames."""
293
+ from oscura.analyzers.protocols.uart import UARTDecoder
294
+
295
+ decoder = UARTDecoder(
296
+ baudrate=config.get("baud_rate", 115200),
297
+ data_bits=config.get("data_bits", 8),
298
+ parity=config.get("parity", "none"),
299
+ stop_bits=config.get("stop_bits", 1),
300
+ )
301
+ return list(decoder.decode(digital_trace))
302
+
303
+
304
+ def _decode_spi(digital_trace: DigitalTrace, config: dict[str, Any]) -> list[Any]:
305
+ """Decode SPI frames."""
306
+ from oscura.analyzers.protocols.spi import SPIDecoder
307
+
308
+ decoder = SPIDecoder(cpol=config.get("clock_polarity", 0), cpha=config.get("clock_phase", 0))
309
+ return list(decoder.decode(digital_trace, clk=digital_trace.data, mosi=digital_trace.data))
310
+
311
+
312
+ def _decode_i2c(digital_trace: DigitalTrace, config: dict[str, Any]) -> list[Any]:
313
+ """Decode I2C frames."""
314
+ from oscura.analyzers.protocols.i2c import I2CDecoder
315
+
316
+ decoder = I2CDecoder()
317
+ sda = digital_trace.data
318
+ edges = np.where(np.diff(sda.astype(int)) != 0)[0]
319
+ scl = np.ones_like(sda, dtype=bool)
320
+ for i, edge in enumerate(edges):
321
+ if i % 2 == 0 and edge + 10 < len(scl):
322
+ scl[edge : edge + 10] = False
323
+ return list(decoder.decode(digital_trace, scl=scl, sda=sda))
324
+
325
+
326
+ def _decode_can(digital_trace: DigitalTrace, config: dict[str, Any]) -> list[Any]:
327
+ """Decode CAN frames."""
328
+ from oscura.analyzers.protocols.can import CANDecoder
329
+
330
+ decoder = CANDecoder(
331
+ bitrate=config.get("baud_rate", 500000), sample_point=config.get("sample_point", 0.75)
300
332
  )
333
+ return list(decoder.decode(digital_trace))
301
334
 
302
335
 
303
336
  def smart_filter(
@@ -330,7 +363,7 @@ def smart_filter(
330
363
  >>> # Or auto-detect
331
364
  >>> clean = osc.smart_filter(noisy, target="auto")
332
365
  """
333
- from oscura.filtering.convenience import (
366
+ from oscura.utils.filtering.convenience import (
334
367
  high_pass,
335
368
  low_pass,
336
369
  median_filter,
@@ -405,11 +438,24 @@ def _detect_noise_type(
405
438
  # Get noise floor estimate
406
439
  noise_floor = np.median(mag_db[10 : len(mag_db) // 4])
407
440
 
408
- # Check for power line hum
409
- if idx_60 < len(mag_db) and mag_db[idx_60] > noise_floor + 20:
410
- return "60hz_hum"
411
- if idx_50 < len(mag_db) and mag_db[idx_50] > noise_floor + 20:
412
- return "50hz_hum"
441
+ # Check for power line hum - must be clear peak at exact frequency
442
+ # Verify it's a local maximum to avoid false positives from spectral leakage
443
+ if idx_60 < len(mag_db) - 5 and idx_60 > 5 and idx_60 < len(freq):
444
+ # Must be within 3 Hz of actual 60 Hz (tighter tolerance)
445
+ if abs(freq[idx_60] - 60) < 3:
446
+ # Check if it's a local maximum (peak, not leakage)
447
+ local_max = mag_db[idx_60 - 2 : idx_60 + 3].max()
448
+ is_peak = mag_db[idx_60] >= local_max - 0.1 # Allow tiny numerical error
449
+ if is_peak and mag_db[idx_60] > noise_floor + 20:
450
+ return "60hz_hum"
451
+ if idx_50 < len(mag_db) - 5 and idx_50 > 5 and idx_50 < len(freq):
452
+ # Must be within 3 Hz of actual 50 Hz (tighter tolerance)
453
+ if abs(freq[idx_50] - 50) < 3:
454
+ # Check if it's a local maximum (peak, not leakage)
455
+ local_max = mag_db[idx_50 - 2 : idx_50 + 3].max()
456
+ is_peak = mag_db[idx_50] >= local_max - 0.1 # Allow tiny numerical error
457
+ if is_peak and mag_db[idx_50] > noise_floor + 20:
458
+ return "50hz_hum"
413
459
 
414
460
  # Check frequency distribution
415
461
  low_power = np.mean(mag_db[1 : len(mag_db) // 10])
oscura/core/__init__.py CHANGED
@@ -39,8 +39,9 @@ from oscura.core.cross_domain import (
39
39
  correlate_results,
40
40
  )
41
41
  from oscura.core.debug import (
42
+ DebugContext,
42
43
  DebugLevel,
43
- debug_context,
44
+ debug_context, # Backward compatibility alias
44
45
  disable_debug,
45
46
  enable_debug,
46
47
  get_debug_level,
@@ -180,6 +181,7 @@ __all__ = [
180
181
  "CorrelationResult",
181
182
  "CrossDomainCorrelator",
182
183
  "CrossDomainInsight",
184
+ "DebugContext",
183
185
  "DebugLevel",
184
186
  "DigitalTrace",
185
187
  # Edge cases (EDGE-001, EDGE-002, EDGE-003)
@@ -251,7 +253,7 @@ __all__ = [
251
253
  "create_progress_tracker",
252
254
  "create_provenance",
253
255
  "create_simple_progress",
254
- "debug_context",
256
+ "debug_context", # Backward compatibility
255
257
  "disable_debug",
256
258
  # Debug (LOG-007)
257
259
  "enable_debug",
@@ -43,7 +43,7 @@ except (ImportError, AttributeError):
43
43
  HAS_GPU = False
44
44
 
45
45
  try:
46
- import numba # type: ignore[import-not-found]
46
+ import numba
47
47
 
48
48
  HAS_NUMBA = True
49
49
  del numba
@@ -51,10 +51,10 @@ except ImportError:
51
51
  HAS_NUMBA = False
52
52
 
53
53
  try:
54
- import dask.array # type: ignore[import-not-found, import-untyped]
54
+ import dask.array # type: ignore[import-not-found] # Optional dependency
55
55
 
56
56
  HAS_DASK = True
57
- del dask # type: ignore[name-defined]
57
+ del dask
58
58
  except ImportError:
59
59
  HAS_DASK = False
60
60