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
oscura/loaders/pcap.py CHANGED
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
27
27
 
28
28
  # Try to import dpkt for full PCAP support
29
29
  try:
30
- import dpkt # type: ignore[import-not-found]
30
+ import dpkt
31
31
 
32
32
  DPKT_AVAILABLE = True
33
33
  except ImportError:
@@ -160,6 +160,135 @@ def load_pcap(
160
160
  )
161
161
 
162
162
 
163
+ def _create_pcap_reader(f: Any, path: Path) -> Any:
164
+ """Create appropriate PCAP reader based on file format.
165
+
166
+ Args:
167
+ f: File handle.
168
+ path: Path to PCAP file.
169
+
170
+ Returns:
171
+ dpkt PCAP or PCAPNG reader.
172
+
173
+ Raises:
174
+ LoaderError: If PCAPNG support is unavailable.
175
+ """
176
+ magic = f.read(4)
177
+ f.seek(0)
178
+ magic_int = struct.unpack("<I", magic)[0]
179
+
180
+ if magic_int == PCAPNG_MAGIC:
181
+ try:
182
+ return dpkt.pcapng.Reader(f)
183
+ except AttributeError:
184
+ raise LoaderError(
185
+ "PCAPNG support requires newer dpkt version",
186
+ file_path=str(path),
187
+ fix_hint="Install dpkt >= 1.9: pip install dpkt>=1.9",
188
+ )
189
+ else:
190
+ return dpkt.pcap.Reader(f)
191
+
192
+
193
+ def _parse_transport_layer(ip: Any, annotations: dict[str, Any]) -> str:
194
+ """Parse TCP/UDP/ICMP transport layer from IP packet.
195
+
196
+ Args:
197
+ ip: dpkt IP object.
198
+ annotations: Annotations dictionary to populate.
199
+
200
+ Returns:
201
+ Protocol name ("TCP", "UDP", "ICMP", or "IP").
202
+ """
203
+ if isinstance(ip.data, dpkt.tcp.TCP):
204
+ tcp = ip.data
205
+ annotations["src_port"] = tcp.sport
206
+ annotations["dst_port"] = tcp.dport
207
+ annotations["layer4_protocol"] = "TCP"
208
+ annotations["tcp_flags"] = tcp.flags
209
+ return "TCP"
210
+
211
+ elif isinstance(ip.data, dpkt.udp.UDP):
212
+ udp = ip.data
213
+ annotations["src_port"] = udp.sport
214
+ annotations["dst_port"] = udp.dport
215
+ annotations["layer4_protocol"] = "UDP"
216
+ return "UDP"
217
+
218
+ elif isinstance(ip.data, dpkt.icmp.ICMP):
219
+ annotations["layer4_protocol"] = "ICMP"
220
+ return "ICMP"
221
+
222
+ return "IP"
223
+
224
+
225
+ def _parse_ethernet_frame(raw_data: bytes, link_type: int) -> tuple[str, dict[str, Any]]:
226
+ """Parse Ethernet frame and extract protocol information.
227
+
228
+ Args:
229
+ raw_data: Raw packet bytes.
230
+ link_type: Link layer type.
231
+
232
+ Returns:
233
+ Tuple of (protocol_name, annotations_dict).
234
+ """
235
+ annotations: dict[str, Any] = {}
236
+ protocol = "RAW"
237
+
238
+ try:
239
+ if link_type != 1: # Not Ethernet
240
+ return protocol, annotations
241
+
242
+ eth = dpkt.ethernet.Ethernet(raw_data)
243
+ annotations["src_mac"] = _format_mac(eth.src)
244
+ annotations["dst_mac"] = _format_mac(eth.dst)
245
+
246
+ # Parse network layer
247
+ if isinstance(eth.data, dpkt.ip.IP):
248
+ ip = eth.data
249
+ annotations["src_ip"] = _format_ip(ip.src)
250
+ annotations["dst_ip"] = _format_ip(ip.dst)
251
+ annotations["layer3_protocol"] = "IP"
252
+ protocol = _parse_transport_layer(ip, annotations)
253
+
254
+ elif isinstance(eth.data, dpkt.ip6.IP6):
255
+ protocol = "IPv6"
256
+ annotations["layer3_protocol"] = "IPv6"
257
+
258
+ elif isinstance(eth.data, dpkt.arp.ARP):
259
+ protocol = "ARP"
260
+ annotations["layer3_protocol"] = "ARP"
261
+
262
+ except Exception:
263
+ # If parsing fails, return defaults
264
+ pass
265
+
266
+ return protocol, annotations
267
+
268
+
269
+ def _matches_protocol_filter(
270
+ protocol: str, annotations: dict[str, Any], protocol_filter: str | None
271
+ ) -> bool:
272
+ """Check if packet matches protocol filter.
273
+
274
+ Args:
275
+ protocol: Packet protocol name.
276
+ annotations: Packet annotations.
277
+ protocol_filter: Filter string.
278
+
279
+ Returns:
280
+ True if packet matches filter (or no filter set).
281
+ """
282
+ if protocol_filter is None:
283
+ return True
284
+
285
+ return (
286
+ annotations.get("layer3_protocol") == protocol_filter
287
+ or annotations.get("layer4_protocol") == protocol_filter
288
+ or protocol == protocol_filter
289
+ )
290
+
291
+
163
292
  def _load_with_dpkt(
164
293
  path: Path,
165
294
  *,
@@ -180,27 +309,8 @@ def _load_with_dpkt(
180
309
  LoaderError: If file cannot be read or dpkt version is incompatible.
181
310
  """
182
311
  try:
183
- with open(path, "rb") as f:
184
- # Detect file format
185
- magic = f.read(4)
186
- f.seek(0)
187
-
188
- magic_int = struct.unpack("<I", magic)[0]
189
-
190
- if magic_int == PCAPNG_MAGIC:
191
- # PCAPNG format
192
- try:
193
- pcap_reader = dpkt.pcapng.Reader(f)
194
- except AttributeError:
195
- raise LoaderError( # noqa: B904
196
- "PCAPNG support requires newer dpkt version",
197
- file_path=str(path),
198
- fix_hint="Install dpkt >= 1.9: pip install dpkt>=1.9",
199
- )
200
- else:
201
- # Standard PCAP format
202
- pcap_reader = dpkt.pcap.Reader(f)
203
-
312
+ with open(path, "rb", buffering=65536) as f:
313
+ pcap_reader = _create_pcap_reader(f, path)
204
314
  packets: list[ProtocolPacket] = []
205
315
  link_type = getattr(pcap_reader, "datalink", lambda: 1)()
206
316
 
@@ -208,62 +318,9 @@ def _load_with_dpkt(
208
318
  if max_packets is not None and len(packets) >= max_packets:
209
319
  break
210
320
 
211
- # Parse Ethernet frame
212
- annotations: dict[str, Any] = {}
213
- protocol = "RAW"
214
-
215
- try:
216
- if link_type == 1: # Ethernet
217
- eth = dpkt.ethernet.Ethernet(raw_data)
218
- annotations["src_mac"] = _format_mac(eth.src)
219
- annotations["dst_mac"] = _format_mac(eth.dst)
220
-
221
- # Parse IP layer
222
- if isinstance(eth.data, dpkt.ip.IP):
223
- ip = eth.data
224
- protocol = "IP"
225
- annotations["src_ip"] = _format_ip(ip.src)
226
- annotations["dst_ip"] = _format_ip(ip.dst)
227
- annotations["layer3_protocol"] = "IP"
228
-
229
- # Parse transport layer
230
- if isinstance(ip.data, dpkt.tcp.TCP):
231
- tcp = ip.data
232
- protocol = "TCP"
233
- annotations["src_port"] = tcp.sport
234
- annotations["dst_port"] = tcp.dport
235
- annotations["layer4_protocol"] = "TCP"
236
- annotations["tcp_flags"] = tcp.flags
237
-
238
- elif isinstance(ip.data, dpkt.udp.UDP):
239
- udp = ip.data
240
- protocol = "UDP"
241
- annotations["src_port"] = udp.sport
242
- annotations["dst_port"] = udp.dport
243
- annotations["layer4_protocol"] = "UDP"
244
-
245
- elif isinstance(ip.data, dpkt.icmp.ICMP):
246
- protocol = "ICMP"
247
- annotations["layer4_protocol"] = "ICMP"
248
-
249
- elif isinstance(eth.data, dpkt.ip6.IP6):
250
- protocol = "IPv6"
251
- annotations["layer3_protocol"] = "IPv6"
252
-
253
- elif isinstance(eth.data, dpkt.arp.ARP):
254
- protocol = "ARP"
255
- annotations["layer3_protocol"] = "ARP"
256
-
257
- except Exception:
258
- # If parsing fails, store raw data
259
- pass
260
-
261
- # Apply protocol filter
262
- if protocol_filter is not None and (
263
- annotations.get("layer3_protocol") != protocol_filter
264
- and annotations.get("layer4_protocol") != protocol_filter
265
- and protocol != protocol_filter
266
- ):
321
+ protocol, annotations = _parse_ethernet_frame(raw_data, link_type)
322
+
323
+ if not _matches_protocol_filter(protocol, annotations, protocol_filter):
267
324
  continue
268
325
 
269
326
  packet = ProtocolPacket(
@@ -312,75 +369,9 @@ def _load_basic(
312
369
  LoaderError: If file cannot be read.
313
370
  """
314
371
  try:
315
- with open(path, "rb") as f:
316
- # Read global header (24 bytes)
317
- header = f.read(24)
318
- if len(header) < 24:
319
- raise FormatError(
320
- "File too small to be a valid PCAP",
321
- file_path=str(path),
322
- expected="At least 24 bytes",
323
- got=f"{len(header)} bytes",
324
- )
325
-
326
- # Parse magic number
327
- magic = struct.unpack("<I", header[:4])[0]
328
-
329
- if magic in (PCAP_MAGIC_LE, PCAP_MAGIC_NS_LE):
330
- byte_order = "<"
331
- nanosecond = magic == PCAP_MAGIC_NS_LE
332
- elif magic in (PCAP_MAGIC_BE, PCAP_MAGIC_NS_BE):
333
- byte_order = ">"
334
- nanosecond = magic == PCAP_MAGIC_NS_BE
335
- elif magic == PCAPNG_MAGIC:
336
- raise LoaderError(
337
- "PCAPNG format requires dpkt library",
338
- file_path=str(path),
339
- fix_hint="Install dpkt: pip install dpkt",
340
- )
341
- else:
342
- raise FormatError(
343
- "Invalid PCAP magic number",
344
- file_path=str(path),
345
- expected="PCAP magic (0xa1b2c3d4)",
346
- got=f"0x{magic:08x}",
347
- )
348
-
349
- # Parse rest of header (version_major, version_minor, thiszone, sigfigs, snaplen, network)
350
- _, _, _, _, snaplen, link_type = struct.unpack(f"{byte_order}HHiIII", header[4:])
351
-
352
- packets: list[ProtocolPacket] = []
353
-
354
- # Read packets
355
- while True:
356
- if max_packets is not None and len(packets) >= max_packets:
357
- break
358
-
359
- # Read packet header (16 bytes)
360
- pkt_header = f.read(16)
361
- if len(pkt_header) < 16:
362
- break
363
-
364
- ts_sec, ts_usec, incl_len, orig_len = struct.unpack(f"{byte_order}IIII", pkt_header)
365
-
366
- # Calculate timestamp
367
- if nanosecond:
368
- timestamp = ts_sec + ts_usec / 1e9
369
- else:
370
- timestamp = ts_sec + ts_usec / 1e6
371
-
372
- # Read packet data
373
- pkt_data = f.read(incl_len)
374
- if len(pkt_data) < incl_len:
375
- break
376
-
377
- packet = ProtocolPacket(
378
- timestamp=timestamp,
379
- protocol="RAW",
380
- data=bytes(pkt_data),
381
- annotations={"original_length": orig_len},
382
- )
383
- packets.append(packet)
372
+ with open(path, "rb", buffering=65536) as f:
373
+ byte_order, nanosecond, snaplen, link_type = _parse_pcap_header(f, path)
374
+ packets = _read_pcap_packets(f, byte_order, nanosecond, max_packets)
384
375
 
385
376
  return PcapPacketList(
386
377
  packets=packets,
@@ -390,10 +381,7 @@ def _load_basic(
390
381
  )
391
382
 
392
383
  except struct.error as e:
393
- raise FormatError(
394
- "Corrupted PCAP file",
395
- file_path=str(path),
396
- ) from e
384
+ raise FormatError("Corrupted PCAP file", file_path=str(path)) from e
397
385
  except Exception as e:
398
386
  if isinstance(e, LoaderError | FormatError):
399
387
  raise
@@ -405,6 +393,93 @@ def _load_basic(
405
393
  ) from e
406
394
 
407
395
 
396
+ def _parse_pcap_header(f: Any, path: Path) -> tuple[str, bool, int, int]:
397
+ """Parse PCAP global header and return format info."""
398
+ header = f.read(24)
399
+ if len(header) < 24:
400
+ raise FormatError(
401
+ "File too small to be a valid PCAP",
402
+ file_path=str(path),
403
+ expected="At least 24 bytes",
404
+ got=f"{len(header)} bytes",
405
+ )
406
+
407
+ # Parse magic number
408
+ magic = struct.unpack("<I", header[:4])[0]
409
+ byte_order, nanosecond = _determine_byte_order(magic, path)
410
+
411
+ # Parse rest of header
412
+ _, _, _, _, snaplen, link_type = struct.unpack(f"{byte_order}HHiIII", header[4:])
413
+ return byte_order, nanosecond, snaplen, link_type
414
+
415
+
416
+ def _determine_byte_order(magic: int, path: Path) -> tuple[str, bool]:
417
+ """Determine byte order and timestamp precision from magic number."""
418
+ if magic in (PCAP_MAGIC_LE, PCAP_MAGIC_NS_LE):
419
+ return "<", magic == PCAP_MAGIC_NS_LE
420
+ elif magic in (PCAP_MAGIC_BE, PCAP_MAGIC_NS_BE):
421
+ return ">", magic == PCAP_MAGIC_NS_BE
422
+ elif magic == PCAPNG_MAGIC:
423
+ raise LoaderError(
424
+ "PCAPNG format requires dpkt library",
425
+ file_path=str(path),
426
+ fix_hint="Install dpkt: pip install dpkt",
427
+ )
428
+ else:
429
+ raise FormatError(
430
+ "Invalid PCAP magic number",
431
+ file_path=str(path),
432
+ expected="PCAP magic (0xa1b2c3d4)",
433
+ got=f"0x{magic:08x}",
434
+ )
435
+
436
+
437
+ def _read_pcap_packets(
438
+ f: Any, byte_order: str, nanosecond: bool, max_packets: int | None
439
+ ) -> list[ProtocolPacket]:
440
+ """Read all packets from PCAP file."""
441
+ DEFAULT_MAX_PACKETS = 1_000_000 # Prevent memory exhaustion on unbounded files
442
+
443
+ packets: list[ProtocolPacket] = []
444
+ effective_max = max_packets if max_packets is not None else DEFAULT_MAX_PACKETS
445
+
446
+ while True:
447
+ if len(packets) >= effective_max:
448
+ break
449
+
450
+ packet = _read_one_packet(f, byte_order, nanosecond)
451
+ if packet is None:
452
+ break
453
+
454
+ packets.append(packet)
455
+
456
+ return packets
457
+
458
+
459
+ def _read_one_packet(f: Any, byte_order: str, nanosecond: bool) -> ProtocolPacket | None:
460
+ """Read one packet from PCAP file."""
461
+ pkt_header = f.read(16)
462
+ if len(pkt_header) < 16:
463
+ return None
464
+
465
+ ts_sec, ts_usec, incl_len, orig_len = struct.unpack(f"{byte_order}IIII", pkt_header)
466
+
467
+ # Calculate timestamp
468
+ timestamp = ts_sec + (ts_usec / 1e9 if nanosecond else ts_usec / 1e6)
469
+
470
+ # Read packet data
471
+ pkt_data = f.read(incl_len)
472
+ if len(pkt_data) < incl_len:
473
+ return None
474
+
475
+ return ProtocolPacket(
476
+ timestamp=timestamp,
477
+ protocol="RAW",
478
+ data=bytes(pkt_data),
479
+ annotations={"original_length": orig_len},
480
+ )
481
+
482
+
408
483
  def _format_mac(mac_bytes: bytes) -> str:
409
484
  """Format MAC address bytes to string.
410
485
 
oscura/loaders/rigol.py CHANGED
@@ -16,6 +16,7 @@ from pathlib import Path
16
16
  from typing import TYPE_CHECKING, Any
17
17
 
18
18
  import numpy as np
19
+ from numpy.typing import NDArray
19
20
 
20
21
  from oscura.core.exceptions import FormatError, LoaderError
21
22
  from oscura.core.types import TraceMetadata, WaveformTrace
@@ -25,7 +26,7 @@ if TYPE_CHECKING:
25
26
 
26
27
  # Try to import RigolWFM for full Rigol support
27
28
  try:
28
- import RigolWFM.wfm as rigol_wfm # type: ignore[import-not-found, import-untyped]
29
+ import RigolWFM.wfm as rigol_wfm # type: ignore[import-untyped] # Optional third-party library
29
30
 
30
31
  RIGOL_WFM_AVAILABLE = True
31
32
  except ImportError:
@@ -100,49 +101,13 @@ def _load_with_rigolwfm(
100
101
  LoaderError: If the file cannot be loaded.
101
102
  """
102
103
  try:
103
- # Try to auto-detect model from filename (e.g., DS1054Z)
104
- model = None
105
- filename_upper = path.name.upper()
106
- if "DS1" in filename_upper or "MSO1" in filename_upper or "DHO" in filename_upper:
107
- if "Z" in filename_upper or "MSO" in filename_upper or "DHO" in filename_upper:
108
- model = "Z"
109
- elif "E" in filename_upper:
110
- model = "E"
111
-
112
- # Try model detection, fallback to trying both models
113
- last_error = None
114
- for try_model in [model] if model else ["Z", "E"]:
115
- try:
116
- wfm = rigol_wfm.Wfm.from_file(str(path), model=try_model)
117
- break
118
- except Exception as e:
119
- last_error = e
120
- continue
121
- else:
122
- # None of the models worked
123
- raise last_error if last_error else RuntimeError("Failed to load WFM file")
124
-
125
- # Get channel data
126
- if hasattr(wfm, "channels") and len(wfm.channels) > channel:
127
- ch = wfm.channels[channel]
128
- data = np.array(ch.volts, dtype=np.float64)
129
- sample_rate = wfm.sample_rate if hasattr(wfm, "sample_rate") else 1e6
130
- vertical_scale = ch.volts_per_div if hasattr(ch, "volts_per_div") else None
131
- vertical_offset = ch.volt_offset if hasattr(ch, "volt_offset") else None
132
- channel_name = f"CH{channel + 1}"
133
- elif hasattr(wfm, "volts"):
134
- # Single channel format
135
- data = np.array(wfm.volts, dtype=np.float64)
136
- sample_rate = wfm.sample_rate if hasattr(wfm, "sample_rate") else 1e6
137
- vertical_scale = wfm.volts_per_div if hasattr(wfm, "volts_per_div") else None
138
- vertical_offset = wfm.volt_offset if hasattr(wfm, "volt_offset") else None
139
- channel_name = "CH1"
140
- else:
141
- raise FormatError(
142
- "No waveform data found in Rigol file",
143
- file_path=str(path),
144
- expected="Rigol channel data",
145
- )
104
+ # Auto-detect model and load waveform
105
+ wfm = _load_rigol_with_model_detection(path)
106
+
107
+ # Extract channel data
108
+ data, sample_rate, vertical_scale, vertical_offset, channel_name = (
109
+ _extract_rigol_channel_data(wfm, channel, str(path))
110
+ )
146
111
 
147
112
  # Build metadata
148
113
  metadata = TraceMetadata(
@@ -157,12 +122,8 @@ def _load_with_rigolwfm(
157
122
  return WaveformTrace(data=data, metadata=metadata)
158
123
 
159
124
  except Exception as e:
160
- # Re-raise FormatError as-is for tests that expect it
161
- # All other exceptions (including kaitaistruct errors) get wrapped
162
125
  if isinstance(e, FormatError):
163
126
  raise
164
- # Wrap other exceptions in LoaderError
165
- # The outer load_rigol_wfm() will catch LoaderError and fall back to basic loader
166
127
  raise LoaderError(
167
128
  "Failed to load Rigol WFM file with RigolWFM library",
168
129
  file_path=str(path),
@@ -171,6 +132,106 @@ def _load_with_rigolwfm(
171
132
  ) from e
172
133
 
173
134
 
135
+ def _detect_rigol_model_from_filename(path: Path) -> str | None:
136
+ """Auto-detect Rigol oscilloscope model from filename.
137
+
138
+ Args:
139
+ path: Path to WFM file.
140
+
141
+ Returns:
142
+ Model string ("Z" or "E") or None if detection fails.
143
+ """
144
+ filename_upper = path.name.upper()
145
+
146
+ # Check for Rigol model indicators in filename
147
+ if "DS1" not in filename_upper and "MSO1" not in filename_upper and "DHO" not in filename_upper:
148
+ return None
149
+
150
+ # Z-series (DS1000Z, MSO1000Z, DHO series)
151
+ if "Z" in filename_upper or "MSO" in filename_upper or "DHO" in filename_upper:
152
+ return "Z"
153
+
154
+ # E-series (DS1000E)
155
+ if "E" in filename_upper:
156
+ return "E"
157
+
158
+ return None
159
+
160
+
161
+ def _load_rigol_with_model_detection(path: Path) -> Any:
162
+ """Load Rigol WFM file with automatic model detection.
163
+
164
+ Args:
165
+ path: Path to WFM file.
166
+
167
+ Returns:
168
+ Loaded waveform object from RigolWFM.
169
+
170
+ Raises:
171
+ RuntimeError: If loading fails with all models.
172
+ """
173
+ model = _detect_rigol_model_from_filename(path)
174
+
175
+ # Try detected model first, then fallback to both models
176
+ models_to_try = [model] if model else ["Z", "E"]
177
+
178
+ last_error = None
179
+ for try_model in models_to_try:
180
+ try:
181
+ return rigol_wfm.Wfm.from_file(str(path), model=try_model)
182
+ except Exception as e:
183
+ last_error = e
184
+ continue
185
+
186
+ # None of the models worked
187
+ raise last_error if last_error else RuntimeError("Failed to load WFM file")
188
+
189
+
190
+ def _extract_rigol_channel_data(
191
+ wfm: Any,
192
+ channel: int,
193
+ file_path: str,
194
+ ) -> tuple[NDArray[np.float64], float, float | None, float | None, str]:
195
+ """Extract channel data from Rigol waveform object.
196
+
197
+ Args:
198
+ wfm: Rigol waveform object from RigolWFM.
199
+ channel: Channel index.
200
+ file_path: File path for error messages.
201
+
202
+ Returns:
203
+ Tuple of (data, sample_rate, vertical_scale, vertical_offset, channel_name).
204
+
205
+ Raises:
206
+ FormatError: If no waveform data found.
207
+ """
208
+ # Multi-channel format
209
+ if hasattr(wfm, "channels") and len(wfm.channels) > channel:
210
+ ch = wfm.channels[channel]
211
+ data = np.array(ch.volts, dtype=np.float64)
212
+ sample_rate = wfm.sample_rate if hasattr(wfm, "sample_rate") else 1e6
213
+ vertical_scale = ch.volts_per_div if hasattr(ch, "volts_per_div") else None
214
+ vertical_offset = ch.volt_offset if hasattr(ch, "volt_offset") else None
215
+ channel_name = f"CH{channel + 1}"
216
+ return data, sample_rate, vertical_scale, vertical_offset, channel_name
217
+
218
+ # Single channel format
219
+ if hasattr(wfm, "volts"):
220
+ data = np.array(wfm.volts, dtype=np.float64)
221
+ sample_rate = wfm.sample_rate if hasattr(wfm, "sample_rate") else 1e6
222
+ vertical_scale = wfm.volts_per_div if hasattr(wfm, "volts_per_div") else None
223
+ vertical_offset = wfm.volt_offset if hasattr(wfm, "volt_offset") else None
224
+ channel_name = "CH1"
225
+ return data, sample_rate, vertical_scale, vertical_offset, channel_name
226
+
227
+ # No recognized format
228
+ raise FormatError(
229
+ "No waveform data found in Rigol file",
230
+ file_path=file_path,
231
+ expected="Rigol channel data",
232
+ )
233
+
234
+
174
235
  def _load_basic(
175
236
  path: Path,
176
237
  *,
@@ -193,7 +254,7 @@ def _load_basic(
193
254
  LoaderError: If the file cannot be read or parsed.
194
255
  """
195
256
  try:
196
- with open(path, "rb") as f:
257
+ with open(path, "rb", buffering=65536) as f:
197
258
  # Read header
198
259
  header = f.read(256)
199
260