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
@@ -128,10 +128,13 @@ class UDSDecoder:
128
128
  # Handle ISO-TP single frame
129
129
  if message.data[0] <= 0x07:
130
130
  first_byte = message.data[1]
131
+ # Negative response needs: PCI + 0x7F + Requested SID + NRC = 4 bytes minimum
132
+ if first_byte == 0x7F:
133
+ return len(message.data) >= 4
131
134
  else:
132
135
  first_byte = message.data[0]
133
136
 
134
- # Negative response
137
+ # Negative response (non-ISO-TP format)
135
138
  if first_byte == 0x7F:
136
139
  return len(message.data) >= 3
137
140
 
@@ -152,93 +155,172 @@ class UDSDecoder:
152
155
 
153
156
  Returns:
154
157
  UDSService, UDSNegativeResponse, or None if not a valid UDS message.
158
+
159
+ Example:
160
+ >>> msg = CANMessage(id=0x7E0, data=bytes([0x02, 0x10, 0x01]))
161
+ >>> service = UDSDecoder.decode_service(msg)
162
+ >>> print(service.name if service else "Invalid")
155
163
  """
156
164
  if len(message.data) < 2:
157
165
  return None
158
166
 
159
- # Check for ISO-TP single frame header (0x0X = length)
160
- iso_tp_offset = 0
161
- uds_length = None
162
- if message.data[0] <= 0x07:
163
- iso_tp_offset = 1
164
- uds_length = message.data[0] # Length field tells us actual UDS payload size
165
-
166
- # Extract UDS data (respecting length if ISO-TP)
167
- if uds_length is not None:
168
- # ISO-TP: extract exactly uds_length bytes starting after length byte
169
- if len(message.data) < iso_tp_offset + uds_length:
170
- return None
171
- data = message.data[iso_tp_offset : iso_tp_offset + uds_length]
167
+ # Extract UDS payload from ISO-TP frame if needed
168
+ data = UDSDecoder._extract_uds_payload(message.data)
169
+ if not data:
170
+ return None
171
+
172
+ # Check for negative response
173
+ if data[0] == 0x7F:
174
+ return UDSDecoder._decode_negative_response(data)
175
+
176
+ # Determine SID and request/response type
177
+ sid_info = UDSDecoder._parse_sid_byte(data[0])
178
+ if sid_info is None:
179
+ return None
180
+
181
+ sid, canonical_sid, is_request = sid_info
182
+ service_name = _SERVICE_NAMES[canonical_sid]
183
+
184
+ # Extract sub-function and payload
185
+ sub_function, payload = UDSDecoder._extract_subfunction_and_payload(
186
+ data, canonical_sid, is_request
187
+ )
188
+
189
+ return UDSService(
190
+ sid=sid,
191
+ name=service_name,
192
+ request=is_request,
193
+ sub_function=sub_function,
194
+ data=payload,
195
+ )
196
+
197
+ @staticmethod
198
+ def _extract_uds_payload(message_data: bytes) -> bytes:
199
+ """Extract UDS payload from CAN message data.
200
+
201
+ Handles ISO-TP single frame format (first byte ≤0x07 indicates length).
202
+
203
+ Args:
204
+ message_data: Raw CAN message data.
205
+
206
+ Returns:
207
+ UDS payload bytes (empty if invalid).
208
+ """
209
+ if message_data[0] <= 0x07:
210
+ # ISO-TP single frame: [length, ...UDS data...]
211
+ uds_length = message_data[0]
212
+ if len(message_data) < 1 + uds_length:
213
+ return b""
214
+ return message_data[1 : 1 + uds_length]
172
215
  else:
173
- # Direct UDS: use all remaining bytes
174
- data = message.data[iso_tp_offset:]
216
+ # Direct UDS: all bytes are UDS data
217
+ return message_data
175
218
 
176
- if not data:
219
+ @staticmethod
220
+ def _decode_negative_response(data: bytes) -> UDSNegativeResponse | None:
221
+ """Decode UDS negative response.
222
+
223
+ Format: [0x7F, requested_SID, NRC]
224
+
225
+ Args:
226
+ data: UDS payload starting with 0x7F.
227
+
228
+ Returns:
229
+ UDSNegativeResponse or None if invalid format.
230
+ """
231
+ if len(data) < 3:
177
232
  return None
178
233
 
179
- first_byte = data[0]
234
+ requested_sid = data[1]
235
+ nrc = data[2]
236
+ nrc_name = _NRC_NAMES.get(nrc, f"unknownNRC_0x{nrc:02X}")
180
237
 
181
- # Negative response: [0x7F, requested_SID, NRC]
182
- if first_byte == 0x7F:
183
- if len(data) < 3:
184
- return None
185
- requested_sid = data[1]
186
- nrc = data[2]
187
- nrc_name = _NRC_NAMES.get(nrc, f"unknownNRC_0x{nrc:02X}")
188
- return UDSNegativeResponse(
189
- requested_sid=requested_sid,
190
- nrc=nrc,
191
- nrc_name=nrc_name,
192
- )
193
-
194
- # Check for positive response (SID + 0x40)
195
- # Response SIDs are in range 0x40-0x7F (corresponding to request SIDs 0x00-0x3F)
196
- # and 0xC0-0xFF (corresponding to request SIDs 0x80-0xBF)
238
+ return UDSNegativeResponse(
239
+ requested_sid=requested_sid,
240
+ nrc=nrc,
241
+ nrc_name=nrc_name,
242
+ )
243
+
244
+ @staticmethod
245
+ def _parse_sid_byte(first_byte: int) -> tuple[int, int, bool] | None:
246
+ """Parse SID and request/response type from first UDS byte.
247
+
248
+ Args:
249
+ first_byte: First byte of UDS payload.
250
+
251
+ Returns:
252
+ Tuple of (actual_sid, canonical_sid, is_request) or None if unknown service.
253
+ - actual_sid: The SID byte from message (0x50 for responses, 0x10 for requests)
254
+ - canonical_sid: The canonical service ID for name lookup (always 0x10)
255
+ - is_request: True if request, False if response
256
+
257
+ Notes:
258
+ - Response SIDs: 0x40-0x7F (request 0x00-0x3F + 0x40)
259
+ - Response SIDs: 0xC0-0xFF (request 0x80-0xBF + 0x40)
260
+ - Request SIDs: 0x00-0x3F, 0x80-0xBF
261
+ """
197
262
  if 0x40 <= first_byte < 0x80:
198
263
  # Positive response to service 0x00-0x3F
199
- sid = first_byte - 0x40
264
+ sid = first_byte # Keep actual response SID (e.g., 0x50)
265
+ canonical_sid = first_byte - 0x40 # Request SID for validation (e.g., 0x10)
200
266
  is_request = False
201
267
  elif first_byte >= 0xC0:
202
268
  # Positive response to service 0x80-0xBF
203
- sid = first_byte - 0x40
269
+ sid = first_byte # Keep actual response SID
270
+ canonical_sid = first_byte - 0x40 # Request SID for validation
204
271
  is_request = False
205
272
  else:
206
273
  # Request (0x00-0x3F or 0x80-0xBF)
207
274
  sid = first_byte
275
+ canonical_sid = first_byte
208
276
  is_request = True
209
277
 
210
- # Unknown service
211
- if sid not in _SERVICE_NAMES:
278
+ # Validate service is known (use canonical request SID)
279
+ if canonical_sid not in _SERVICE_NAMES:
212
280
  return None
213
281
 
214
- service_name = _SERVICE_NAMES[sid]
282
+ return (sid, canonical_sid, is_request)
283
+
284
+ @staticmethod
285
+ def _extract_subfunction_and_payload(
286
+ data: bytes, sid: int, is_request: bool
287
+ ) -> tuple[int | None, bytes]:
288
+ """Extract sub-function and payload from UDS service data.
289
+
290
+ Args:
291
+ data: UDS payload bytes.
292
+ sid: Service ID.
293
+ is_request: True if request, False if response.
215
294
 
216
- # Extract sub-function if present
295
+ Returns:
296
+ Tuple of (sub_function, payload_bytes).
297
+
298
+ Notes:
299
+ Some services echo sub-function in responses: 0x10, 0x11, 0x27, 0x28, 0x31, 0x3E, 0x85.
300
+ """
217
301
  sub_function = None
218
302
  payload_offset = 1
219
303
 
220
- if is_request and sid in _SERVICES_WITH_SUBFUNCTIONS:
304
+ if sid not in _SERVICES_WITH_SUBFUNCTIONS:
305
+ # No sub-function for this service
306
+ payload = data[payload_offset:] if len(data) > payload_offset else b""
307
+ return (sub_function, payload)
308
+
309
+ # Extract sub-function if data contains it
310
+ if is_request:
221
311
  if len(data) >= 2:
222
- # Sub-function may have suppress positive response bit (0x80)
312
+ # Mask off suppress positive response bit (0x80)
313
+ sub_function = data[1] & 0x7F
314
+ payload_offset = 2
315
+ else:
316
+ # Response may echo sub-function for certain services
317
+ _SERVICES_WITH_ECHO = {0x10, 0x11, 0x27, 0x28, 0x31, 0x3E, 0x85}
318
+ if sid in _SERVICES_WITH_ECHO and len(data) >= 2:
223
319
  sub_function = data[1] & 0x7F
224
320
  payload_offset = 2
225
- elif not is_request and sid in _SERVICES_WITH_SUBFUNCTIONS:
226
- # Response may echo sub-function for some services
227
- if sid in {0x10, 0x11, 0x27, 0x28, 0x31, 0x3E, 0x85}:
228
- if len(data) >= 2:
229
- sub_function = data[1] & 0x7F
230
- payload_offset = 2
231
-
232
- # Extract remaining data payload
233
- payload = data[payload_offset:] if len(data) > payload_offset else b""
234
321
 
235
- return UDSService(
236
- sid=sid,
237
- name=service_name,
238
- request=is_request,
239
- sub_function=sub_function,
240
- data=payload,
241
- )
322
+ payload = data[payload_offset:] if len(data) > payload_offset else b""
323
+ return (sub_function, payload)
242
324
 
243
325
  @staticmethod
244
326
  def get_service_name(sid: int) -> str:
@@ -16,7 +16,8 @@ class UDSService:
16
16
  """A decoded UDS service.
17
17
 
18
18
  Attributes:
19
- sid: Service ID (0x10-0xFF).
19
+ sid: Service ID (0x10-0xFF). For responses, this is the response SID (e.g., 0x50),
20
+ not the request SID (0x10).
20
21
  name: Human-readable service name.
21
22
  request: True if request message, False if positive response.
22
23
  sub_function: Sub-function byte (if applicable).
@@ -29,6 +30,11 @@ class UDSService:
29
30
  sub_function: int | None = None
30
31
  data: bytes = b""
31
32
 
33
+ @property
34
+ def is_response(self) -> bool:
35
+ """True if this is a response message."""
36
+ return not self.request
37
+
32
38
  def __repr__(self) -> str:
33
39
  """Human-readable representation."""
34
40
  msg_type = "Request" if self.request else "Response"
@@ -63,7 +63,7 @@ def plot_bus_timeline(
63
63
  msg_list: list[CANMessage] = [plot_messages]
64
64
  else:
65
65
  # Must be list[CANMessage] from slice
66
- msg_list = plot_messages # type: ignore[assignment]
66
+ msg_list = plot_messages
67
67
 
68
68
  # Extract timestamps and IDs
69
69
  timestamps = [_get_timestamp(msg) for msg in msg_list]
oscura/cli/analyze.py ADDED
@@ -0,0 +1,348 @@
1
+ """Oscura Analyze Command - Full Analysis Workflow.
2
+
3
+ Provides CLI for running complete analysis workflows on waveform files with
4
+ automatic protocol detection, signal characterization, and comprehensive reporting.
5
+
6
+
7
+ Example:
8
+ $ oscura analyze signal.wfm
9
+ $ oscura analyze capture.wfm --protocol uart --export-dir output/
10
+ $ oscura analyze data.wfm --interactive
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ import click
20
+
21
+ from oscura.cli.main import format_output
22
+ from oscura.cli.progress import ProgressReporter
23
+ from oscura.sessions.legacy import Session
24
+
25
+ logger = logging.getLogger("oscura.cli.analyze")
26
+
27
+
28
+ @click.command()
29
+ @click.argument("file", type=click.Path(exists=True))
30
+ @click.option(
31
+ "--protocol",
32
+ type=str,
33
+ default="auto",
34
+ help="Protocol hint (auto for auto-detection).",
35
+ )
36
+ @click.option(
37
+ "--export-dir",
38
+ type=click.Path(),
39
+ default=None,
40
+ help="Directory to export results (default: ./oscura_output).",
41
+ )
42
+ @click.option(
43
+ "--interactive",
44
+ "-i",
45
+ is_flag=True,
46
+ help="Interactive mode with prompts.",
47
+ )
48
+ @click.option(
49
+ "--output",
50
+ type=click.Choice(["json", "csv", "html", "table"], case_sensitive=False),
51
+ default="table",
52
+ help="Output format (default: table).",
53
+ )
54
+ @click.option(
55
+ "--save-session",
56
+ type=click.Path(),
57
+ default=None,
58
+ help="Save analysis session to file (.tks).",
59
+ )
60
+ @click.pass_context
61
+ def analyze(
62
+ ctx: click.Context,
63
+ file: str,
64
+ protocol: str,
65
+ export_dir: str | None,
66
+ interactive: bool,
67
+ output: str,
68
+ save_session: str | None,
69
+ ) -> None:
70
+ """Run full analysis workflow on waveform file.
71
+
72
+ Performs comprehensive analysis including signal characterization,
73
+ protocol detection and decoding, spectral analysis, and generates
74
+ detailed reports.
75
+
76
+ Args:
77
+ ctx: Click context object.
78
+ file: Path to waveform file to analyze.
79
+ protocol: Protocol hint or 'auto' for detection.
80
+ export_dir: Directory for exported results.
81
+ interactive: Enable interactive prompts.
82
+ output: Output format.
83
+ save_session: Path to save session file.
84
+
85
+ Examples:
86
+
87
+ \b
88
+ # Full auto analysis
89
+ $ oscura analyze capture.wfm
90
+
91
+ \b
92
+ # With protocol hint and export
93
+ $ oscura analyze signal.wfm \\
94
+ --protocol uart \\
95
+ --export-dir analysis_results/
96
+
97
+ \b
98
+ # Interactive mode
99
+ $ oscura analyze data.wfm --interactive
100
+ """
101
+ verbose = ctx.obj.get("verbose", 0)
102
+ quiet = ctx.obj.get("quiet", False)
103
+
104
+ if verbose:
105
+ logger.info(f"Analyzing: {file}")
106
+
107
+ try:
108
+ results = _perform_analysis_workflow(
109
+ file, protocol, export_dir, interactive, quiet, save_session
110
+ )
111
+ formatted = format_output(results, output)
112
+ click.echo(formatted)
113
+
114
+ except Exception as e:
115
+ logger.error(f"Analysis failed: {e}")
116
+ if verbose > 1:
117
+ raise
118
+ click.echo(f"Error: {e}", err=True)
119
+ ctx.exit(1)
120
+
121
+
122
+ def _perform_analysis_workflow(
123
+ file: str,
124
+ protocol: str,
125
+ export_dir: str | None,
126
+ interactive: bool,
127
+ quiet: bool,
128
+ save_session: str | None,
129
+ ) -> dict[str, Any]:
130
+ """Perform complete analysis workflow."""
131
+ from oscura.loaders import load
132
+
133
+ progress = ProgressReporter(quiet=quiet, stages=5)
134
+
135
+ # Stage 1: Load file
136
+ progress.start_stage("Loading file")
137
+ trace = load(file)
138
+ progress.complete_stage()
139
+
140
+ # Stage 2: Signal characterization
141
+ progress.start_stage("Characterizing signal")
142
+ signal_char = _characterize_signal(trace)
143
+ progress.complete_stage()
144
+
145
+ # Stage 3: Protocol detection
146
+ progress.start_stage("Detecting protocol")
147
+ protocol_info = _detect_and_prepare_protocol(trace, protocol, interactive)
148
+ progress.complete_stage()
149
+
150
+ # Stage 4: Protocol decoding
151
+ progress.start_stage("Decoding protocol")
152
+ decoded = _decode_protocol(trace, protocol_info["protocol"])
153
+ progress.complete_stage()
154
+
155
+ # Stage 5: Generate report
156
+ progress.start_stage("Generating report")
157
+ results = _build_analysis_results(
158
+ file, signal_char, protocol_info, decoded, export_dir, save_session, trace
159
+ )
160
+ progress.complete_stage()
161
+ progress.finish()
162
+
163
+ return results
164
+
165
+
166
+ def _detect_and_prepare_protocol(trace: Any, protocol: str, interactive: bool) -> dict[str, Any]:
167
+ """Detect and prepare protocol information."""
168
+ if protocol == "auto":
169
+ detected = _detect_protocol(trace, interactive=interactive)
170
+ protocol = detected["protocol"]
171
+ return {"protocol": protocol, "auto_detected": protocol == "auto"}
172
+
173
+
174
+ def _build_analysis_results(
175
+ file: str,
176
+ signal_char: dict[str, Any],
177
+ protocol_info: dict[str, Any],
178
+ decoded: dict[str, Any],
179
+ export_dir: str | None,
180
+ save_session: str | None,
181
+ trace: Any,
182
+ ) -> dict[str, Any]:
183
+ """Build final analysis results with optional exports."""
184
+ results = {
185
+ "file": str(Path(file).name),
186
+ **signal_char,
187
+ **protocol_info,
188
+ **decoded,
189
+ }
190
+
191
+ if export_dir:
192
+ export_path = Path(export_dir)
193
+ export_path.mkdir(parents=True, exist_ok=True)
194
+ _export_results(results, export_path)
195
+ results["export_dir"] = str(export_path)
196
+
197
+ if save_session:
198
+ session = Session(name=Path(file).stem)
199
+ session.add_trace("main", trace)
200
+ session.metadata["analysis_results"] = results
201
+ session.save(save_session)
202
+ results["session_file"] = save_session
203
+
204
+ return results
205
+
206
+
207
+ def _characterize_signal(trace: Any) -> dict[str, Any]:
208
+ """Characterize signal properties.
209
+
210
+ Args:
211
+ trace: Waveform trace to characterize.
212
+
213
+ Returns:
214
+ Dictionary of signal characteristics.
215
+ """
216
+ import numpy as np
217
+
218
+ from oscura.analyzers.waveform.measurements import fall_time, rise_time
219
+
220
+ data = trace.data
221
+ sample_rate = trace.metadata.sample_rate
222
+
223
+ rt = rise_time(trace)
224
+ ft = fall_time(trace)
225
+
226
+ return {
227
+ "sample_rate": f"{sample_rate / 1e6:.1f} MHz",
228
+ "samples": len(data),
229
+ "duration": f"{len(data) / sample_rate * 1e3:.3f} ms",
230
+ "amplitude": f"{float(data.max() - data.min()):.3f} V",
231
+ "rise_time": f"{rt * 1e9:.2f} ns" if not np.isnan(rt) else "N/A",
232
+ "fall_time": f"{ft * 1e9:.2f} ns" if not np.isnan(ft) else "N/A",
233
+ }
234
+
235
+
236
+ def _detect_protocol(trace: Any, interactive: bool = False) -> dict[str, Any]:
237
+ """Detect protocol from trace.
238
+
239
+ Args:
240
+ trace: Trace to analyze.
241
+ interactive: If True, prompt for confirmation.
242
+
243
+ Returns:
244
+ Detection results.
245
+ """
246
+ from oscura.inference.protocol import detect_protocol
247
+
248
+ detection = detect_protocol(trace, min_confidence=0.5, return_candidates=True)
249
+
250
+ if interactive and detection.get("confidence", 0) < 0.9:
251
+ click.echo(f"\nDetected protocol: {detection['protocol']}")
252
+ click.echo(f"Confidence: {detection['confidence']:.1%}")
253
+
254
+ if not click.confirm("Use this protocol?", default=True):
255
+ # Show candidates
256
+ candidates = detection.get("candidates", [])
257
+ if candidates:
258
+ click.echo("\nOther candidates:")
259
+ for i, cand in enumerate(candidates[:5], 1):
260
+ click.echo(f" {i}. {cand['protocol']} ({cand['confidence']:.1%})")
261
+
262
+ choice = click.prompt("Select protocol (1-5, or 0 for manual)", type=int, default=1)
263
+ if 1 <= choice <= len(candidates):
264
+ detection = candidates[choice - 1]
265
+ elif choice == 0:
266
+ manual = click.prompt("Enter protocol name", type=str)
267
+ detection = {"protocol": manual, "confidence": 1.0}
268
+
269
+ return detection
270
+
271
+
272
+ def _decode_protocol(trace: Any, protocol: str) -> dict[str, Any]:
273
+ """Decode protocol from trace.
274
+
275
+ Args:
276
+ trace: Trace to decode.
277
+ protocol: Protocol name.
278
+
279
+ Returns:
280
+ Decoding results.
281
+ """
282
+ from oscura.core.types import DigitalTrace
283
+
284
+ # Convert to digital if needed
285
+ if not isinstance(trace, DigitalTrace):
286
+ import numpy as np
287
+
288
+ threshold = (np.max(trace.data) + np.min(trace.data)) / 2
289
+ digital_data = trace.data > threshold
290
+ digital_trace = DigitalTrace(data=digital_data, metadata=trace.metadata)
291
+ else:
292
+ digital_trace = trace
293
+
294
+ # Decode based on protocol
295
+ packets: list[Any] = []
296
+ if protocol.lower() == "uart":
297
+ from oscura.analyzers.protocols.uart import UARTDecoder
298
+
299
+ uart_decoder = UARTDecoder(baudrate=0) # Auto-detect
300
+ packets = list(uart_decoder.decode(digital_trace))
301
+ elif protocol.lower() == "spi":
302
+ from oscura.analyzers.protocols.spi import SPIDecoder
303
+
304
+ spi_decoder = SPIDecoder()
305
+ packets = list(
306
+ spi_decoder.decode(
307
+ clk=digital_trace.data,
308
+ mosi=digital_trace.data,
309
+ sample_rate=trace.metadata.sample_rate,
310
+ )
311
+ )
312
+ elif protocol.lower() == "i2c":
313
+ from oscura.analyzers.protocols.i2c import I2CDecoder
314
+
315
+ i2c_decoder = I2CDecoder()
316
+ packets = list(
317
+ i2c_decoder.decode(
318
+ scl=digital_trace.data,
319
+ sda=digital_trace.data,
320
+ sample_rate=trace.metadata.sample_rate,
321
+ )
322
+ )
323
+
324
+ return {
325
+ "packets_decoded": len(packets),
326
+ "errors": sum(1 for p in packets if p.errors),
327
+ }
328
+
329
+
330
+ def _export_results(results: dict[str, Any], export_dir: Path) -> None:
331
+ """Export results to directory.
332
+
333
+ Args:
334
+ results: Results dictionary.
335
+ export_dir: Export directory path.
336
+ """
337
+ import json
338
+
339
+ # Export JSON
340
+ json_path = export_dir / "analysis_results.json"
341
+ with open(json_path, "w") as f:
342
+ json.dump(results, f, indent=2, default=str)
343
+
344
+ # Export HTML report
345
+ html_path = export_dir / "analysis_report.html"
346
+ html_content = format_output(results, "html")
347
+ with open(html_path, "w") as f:
348
+ f.write(html_content)