oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl

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