oscura 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (513) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -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)