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
@@ -17,6 +17,142 @@ from oscura.automotive.can.models import CANMessage, CANMessageList
17
17
  __all__ = ["load_csv_can"]
18
18
 
19
19
 
20
+ def _detect_csv_columns(fieldnames: list[str] | None) -> dict[str, str]:
21
+ """Detect required column names from CSV header.
22
+
23
+ Args:
24
+ fieldnames: List of column names from CSV header.
25
+
26
+ Returns:
27
+ Dictionary mapping logical names to actual column names.
28
+
29
+ Raises:
30
+ ValueError: If CSV has no header or missing required columns.
31
+ """
32
+ if not fieldnames:
33
+ raise ValueError("CSV file has no header row")
34
+
35
+ # Normalize column names to lowercase
36
+ fieldnames_lower = [name.lower().strip() for name in fieldnames]
37
+
38
+ # Find required columns
39
+ timestamp_col = None
40
+ id_col = None
41
+ data_col = None
42
+
43
+ for col in fieldnames_lower:
44
+ if "timestamp" in col or col == "time" or col == "t":
45
+ timestamp_col = col
46
+ elif "id" in col or col == "can_id" or col == "arbitration_id":
47
+ id_col = col
48
+ elif "data" in col or col == "payload" or col == "bytes":
49
+ data_col = col
50
+
51
+ if not all([timestamp_col is not None, id_col is not None, data_col is not None]):
52
+ raise ValueError(
53
+ f"CSV file missing required columns. "
54
+ f"Found: {fieldnames_lower}. "
55
+ f"Need: timestamp, id, data"
56
+ )
57
+
58
+ # All columns are guaranteed non-None here due to validation above
59
+ return {
60
+ "timestamp": str(timestamp_col),
61
+ "id": str(id_col),
62
+ "data": str(data_col),
63
+ }
64
+
65
+
66
+ def _parse_csv_row(
67
+ row_dict: dict[str, str],
68
+ column_mapping: dict[str, str],
69
+ messages: CANMessageList,
70
+ ) -> None:
71
+ """Parse a single CSV row into a CAN message.
72
+
73
+ Args:
74
+ row_dict: Dictionary of row data from CSV.
75
+ column_mapping: Mapping of logical names to column names.
76
+ messages: Message list to append to.
77
+ """
78
+ # Create lowercase dict for case-insensitive access
79
+ row = {k.lower().strip(): v for k, v in row_dict.items()}
80
+
81
+ try:
82
+ # Parse timestamp
83
+ timestamp = float(row[column_mapping["timestamp"]])
84
+
85
+ # Parse ID (handle hex or decimal)
86
+ arb_id = _parse_can_id(row[column_mapping["id"]])
87
+
88
+ # Parse data bytes
89
+ data_bytes = _parse_data_bytes(row[column_mapping["data"]])
90
+
91
+ # Determine if extended (>11 bits = 0x7FF)
92
+ is_extended = arb_id > 0x7FF
93
+
94
+ # Create and append message
95
+ can_msg = CANMessage(
96
+ arbitration_id=arb_id,
97
+ timestamp=timestamp,
98
+ data=data_bytes,
99
+ is_extended=is_extended,
100
+ is_fd=False,
101
+ channel=0,
102
+ )
103
+ messages.append(can_msg)
104
+
105
+ except (ValueError, KeyError):
106
+ # Skip malformed rows silently
107
+ pass
108
+
109
+
110
+ def _parse_can_id(id_str: str) -> int:
111
+ """Parse CAN ID from string (hex or decimal).
112
+
113
+ Args:
114
+ id_str: CAN ID string.
115
+
116
+ Returns:
117
+ CAN ID as integer.
118
+
119
+ Raises:
120
+ ValueError: If ID cannot be parsed.
121
+ """
122
+ id_str = id_str.strip()
123
+
124
+ if id_str.startswith("0x") or id_str.startswith("0X"):
125
+ return int(id_str, 16)
126
+
127
+ # Try as int first, then hex
128
+ try:
129
+ return int(id_str)
130
+ except ValueError:
131
+ return int(id_str, 16)
132
+
133
+
134
+ def _parse_data_bytes(data_str: str) -> bytes:
135
+ """Parse data bytes from hex string.
136
+
137
+ Args:
138
+ data_str: Data bytes as hex string.
139
+
140
+ Returns:
141
+ Parsed bytes.
142
+
143
+ Raises:
144
+ ValueError: If data cannot be parsed.
145
+ """
146
+ # Remove common separators and spaces
147
+ data_str = data_str.strip().replace(" ", "").replace(":", "").replace("-", "")
148
+
149
+ # Remove 0x prefix if present
150
+ if data_str.startswith("0x") or data_str.startswith("0X"):
151
+ data_str = data_str[2:]
152
+
153
+ return bytes.fromhex(data_str)
154
+
155
+
20
156
  def load_csv_can(file_path: Path | str, delimiter: str = ",") -> CANMessageList:
21
157
  """Load CAN messages from a CSV file.
22
158
 
@@ -54,79 +190,14 @@ def load_csv_can(file_path: Path | str, delimiter: str = ",") -> CANMessageList:
54
190
  with open(path, encoding="utf-8") as f:
55
191
  reader = csv.DictReader(f, delimiter=delimiter)
56
192
 
57
- # Try to detect column names
58
- if not reader.fieldnames:
59
- raise ValueError("CSV file has no header row")
60
-
61
- # Normalize column names to lowercase for easier matching
62
- fieldnames = [name.lower().strip() for name in reader.fieldnames]
63
-
64
- # Find required columns
65
- timestamp_col = None
66
- id_col = None
67
- data_col = None
68
-
69
- for col in fieldnames:
70
- if "timestamp" in col or col == "time" or col == "t":
71
- timestamp_col = col
72
- elif "id" in col or col == "can_id" or col == "arbitration_id":
73
- id_col = col
74
- elif "data" in col or col == "payload" or col == "bytes":
75
- data_col = col
76
-
77
- if not all([timestamp_col, id_col, data_col]):
78
- raise ValueError(
79
- f"CSV file missing required columns. "
80
- f"Found: {fieldnames}. "
81
- f"Need: timestamp, id, data"
82
- )
83
-
84
- # Parse messages
193
+ # Detect and validate column layout
194
+ column_mapping = _detect_csv_columns(
195
+ list(reader.fieldnames) if reader.fieldnames else None
196
+ )
197
+
198
+ # Parse all message rows
85
199
  for row_dict in reader:
86
- # Create lowercase dict for case-insensitive access
87
- row = {k.lower().strip(): v for k, v in row_dict.items()}
88
-
89
- try:
90
- # Parse timestamp
91
- timestamp = float(row[timestamp_col])
92
-
93
- # Parse ID (handle hex or decimal)
94
- id_str = row[id_col].strip()
95
- if id_str.startswith("0x") or id_str.startswith("0X"):
96
- arb_id = int(id_str, 16)
97
- else:
98
- # Try as int first, then hex
99
- try:
100
- arb_id = int(id_str)
101
- except ValueError:
102
- arb_id = int(id_str, 16)
103
-
104
- # Parse data bytes
105
- data_str = row[data_col].strip()
106
- # Remove common separators and spaces
107
- data_str = data_str.replace(" ", "").replace(":", "").replace("-", "")
108
- # Remove 0x prefix if present
109
- if data_str.startswith("0x") or data_str.startswith("0X"):
110
- data_str = data_str[2:]
111
- data_bytes = bytes.fromhex(data_str)
112
-
113
- # Determine if extended (>11 bits = 0x7FF)
114
- is_extended = arb_id > 0x7FF
115
-
116
- # Create message
117
- can_msg = CANMessage(
118
- arbitration_id=arb_id,
119
- timestamp=timestamp,
120
- data=data_bytes,
121
- is_extended=is_extended,
122
- is_fd=False,
123
- channel=0,
124
- )
125
- messages.append(can_msg)
126
-
127
- except (ValueError, KeyError):
128
- # Skip malformed rows
129
- continue
200
+ _parse_csv_row(row_dict, column_mapping, messages)
130
201
 
131
202
  except Exception as e:
132
203
  raise ValueError(f"Failed to parse CSV file {path}: {e}") from e
@@ -27,6 +27,32 @@ def detect_format(file_path: Path | str) -> str:
27
27
  path = Path(file_path)
28
28
 
29
29
  # Check extension first
30
+ ext_format = _detect_by_extension(path)
31
+ if ext_format != "unknown":
32
+ return ext_format
33
+
34
+ # Check binary file contents
35
+ binary_format = _detect_by_binary_header(path)
36
+ if binary_format != "unknown":
37
+ return binary_format
38
+
39
+ # Check text file contents
40
+ text_format = _detect_by_text_content(path)
41
+ if text_format != "unknown":
42
+ return text_format
43
+
44
+ return "unknown"
45
+
46
+
47
+ def _detect_by_extension(path: Path) -> str:
48
+ """Detect format by file extension.
49
+
50
+ Args:
51
+ path: Path to the file.
52
+
53
+ Returns:
54
+ Format name or 'unknown'.
55
+ """
30
56
  ext = path.suffix.lower()
31
57
 
32
58
  if ext == ".blf":
@@ -40,7 +66,18 @@ def detect_format(file_path: Path | str) -> str:
40
66
  elif ext in [".pcap", ".pcapng"]:
41
67
  return "pcap"
42
68
 
43
- # If extension is ambiguous, check file contents
69
+ return "unknown"
70
+
71
+
72
+ def _detect_by_binary_header(path: Path) -> str:
73
+ """Detect format by binary file header.
74
+
75
+ Args:
76
+ path: Path to the file.
77
+
78
+ Returns:
79
+ Format name or 'unknown'.
80
+ """
44
81
  try:
45
82
  with open(path, "rb") as f:
46
83
  header = f.read(16)
@@ -60,7 +97,18 @@ def detect_format(file_path: Path | str) -> str:
60
97
  except Exception:
61
98
  pass
62
99
 
63
- # Try as text file
100
+ return "unknown"
101
+
102
+
103
+ def _detect_by_text_content(path: Path) -> str:
104
+ """Detect format by text file content.
105
+
106
+ Args:
107
+ path: Path to the file.
108
+
109
+ Returns:
110
+ Format name or 'unknown'.
111
+ """
64
112
  try:
65
113
  with open(path, encoding="utf-8") as f:
66
114
  first_line = f.readline().strip()
@@ -47,7 +47,7 @@ def load_mdf(file_path: Path | str) -> CANMessageList:
47
47
  >>> print(f"Unique IDs: {len(messages.unique_ids())}")
48
48
  """
49
49
  try:
50
- from asammdf import MDF # type: ignore[import-untyped]
50
+ from asammdf import MDF
51
51
  except ImportError as e:
52
52
  raise ImportError(
53
53
  "asammdf is required for MDF/MF4 file support. "
@@ -142,63 +142,104 @@ def _extract_structured_can_frames(
142
142
  timestamps: Array of timestamps.
143
143
  messages: CANMessageList to append to.
144
144
  """
145
- # Common field names in structured CAN logging
146
- id_fields = ["ID", "id", "BusID", "Identifier", "ArbitrationID"]
147
- data_fields = ["Data", "data", "DataBytes", "Payload"]
148
- dlc_fields = ["DLC", "dlc", "DataLength"]
149
-
150
- # Find which fields are present
151
145
  field_names = samples.dtype.names
152
146
  if not field_names:
153
147
  return
154
148
 
155
- id_field = next((f for f in id_fields if f in field_names), None)
156
- data_field = next((f for f in data_fields if f in field_names), None)
157
- dlc_field = next((f for f in dlc_fields if f in field_names), None)
158
-
159
- if not id_field:
149
+ field_map = _find_can_field_mapping(field_names)
150
+ if not field_map["id_field"]:
160
151
  return
161
152
 
162
153
  for i in range(len(samples)):
163
154
  try:
164
- # Extract CAN ID
165
- arb_id = int(samples[id_field][i])
166
-
167
- # Extract data bytes
168
- if data_field:
169
- data_bytes = samples[data_field][i]
170
- if isinstance(data_bytes, bytes):
171
- data = data_bytes
172
- else:
173
- # Convert array to bytes
174
- data = bytes(data_bytes)
175
- else:
176
- data = b""
177
-
178
- # Extract DLC if available
179
- if dlc_field:
180
- dlc = int(samples[dlc_field][i])
181
- data = data[:dlc]
182
-
183
- # Determine if extended ID
184
- is_extended = arb_id > 0x7FF
185
-
186
- # Create message
187
- can_msg = CANMessage(
188
- arbitration_id=arb_id,
189
- timestamp=float(timestamps[i]),
190
- data=data,
191
- is_extended=is_extended,
192
- is_fd=False, # MDF doesn't typically indicate CAN-FD
193
- channel=0, # Channel info not always available in MDF
194
- )
155
+ can_msg = _extract_can_message_from_sample(samples, timestamps, i, field_map)
195
156
  messages.append(can_msg)
196
-
197
157
  except (IndexError, ValueError, TypeError):
198
- # Skip malformed frames
199
158
  continue
200
159
 
201
160
 
161
+ def _find_can_field_mapping(field_names: tuple[str, ...]) -> dict[str, str | None]:
162
+ """Find mapping of CAN field names in structured array.
163
+
164
+ Args:
165
+ field_names: Field names from structured array.
166
+
167
+ Returns:
168
+ Dictionary mapping field types to actual field names.
169
+ """
170
+ id_fields = ["ID", "id", "BusID", "Identifier", "ArbitrationID"]
171
+ data_fields = ["Data", "data", "DataBytes", "Payload"]
172
+ dlc_fields = ["DLC", "dlc", "DataLength"]
173
+
174
+ return {
175
+ "id_field": next((f for f in id_fields if f in field_names), None),
176
+ "data_field": next((f for f in data_fields if f in field_names), None),
177
+ "dlc_field": next((f for f in dlc_fields if f in field_names), None),
178
+ }
179
+
180
+
181
+ def _extract_can_message_from_sample(
182
+ samples: npt.NDArray[Any],
183
+ timestamps: npt.NDArray[Any],
184
+ index: int,
185
+ field_map: dict[str, str | None],
186
+ ) -> CANMessage:
187
+ """Extract single CAN message from sample.
188
+
189
+ Args:
190
+ samples: Structured array.
191
+ timestamps: Timestamp array.
192
+ index: Sample index.
193
+ field_map: Field name mapping.
194
+
195
+ Returns:
196
+ CANMessage instance.
197
+ """
198
+ arb_id = int(samples[field_map["id_field"]][index])
199
+ data = _extract_can_data_field(samples, index, field_map)
200
+ is_extended = arb_id > 0x7FF
201
+
202
+ return CANMessage(
203
+ arbitration_id=arb_id,
204
+ timestamp=float(timestamps[index]),
205
+ data=data,
206
+ is_extended=is_extended,
207
+ is_fd=False,
208
+ channel=0,
209
+ )
210
+
211
+
212
+ def _extract_can_data_field(
213
+ samples: npt.NDArray[Any], index: int, field_map: dict[str, str | None]
214
+ ) -> bytes:
215
+ """Extract data bytes from sample.
216
+
217
+ Args:
218
+ samples: Structured array.
219
+ index: Sample index.
220
+ field_map: Field name mapping.
221
+
222
+ Returns:
223
+ Data bytes.
224
+ """
225
+ data_field = field_map["data_field"]
226
+ if not data_field:
227
+ return b""
228
+
229
+ data_bytes = samples[data_field][index]
230
+ if isinstance(data_bytes, bytes):
231
+ data = data_bytes
232
+ else:
233
+ data = bytes(data_bytes)
234
+
235
+ dlc_field = field_map["dlc_field"]
236
+ if dlc_field:
237
+ dlc = int(samples[dlc_field][index])
238
+ data = data[:dlc]
239
+
240
+ return data
241
+
242
+
202
243
  def _extract_raw_can_frames(
203
244
  samples: npt.NDArray[Any], timestamps: npt.NDArray[Any], messages: CANMessageList
204
245
  ) -> None:
@@ -15,13 +15,9 @@ Requirements:
15
15
  from __future__ import annotations
16
16
 
17
17
  from pathlib import Path
18
- from typing import TYPE_CHECKING
19
18
 
20
19
  from oscura.automotive.can.models import CANMessage, CANMessageList
21
20
 
22
- if TYPE_CHECKING:
23
- from scapy.packet import Packet # type: ignore[import-not-found]
24
-
25
21
  __all__ = ["load_pcap"]
26
22
 
27
23
 
@@ -58,67 +54,12 @@ def load_pcap(file_path: Path | str) -> CANMessageList:
58
54
  if not path.exists():
59
55
  raise FileNotFoundError(f"PCAP file not found: {path}")
60
56
 
61
- try:
62
- from scapy.all import rdpcap # type: ignore[import-not-found]
63
- from scapy.layers.can import CAN # type: ignore[import-not-found]
64
- except ImportError as e:
65
- msg = "scapy library is required for PCAP loading. Install with: uv pip install scapy"
66
- raise ImportError(msg) from e
67
-
57
+ rdpcap, CAN = _import_scapy_modules()
68
58
  messages = CANMessageList()
69
59
 
70
60
  try:
71
- # Read PCAP file
72
61
  packets = rdpcap(str(path))
73
-
74
- # Extract CAN frames
75
- first_timestamp: float | None = None
76
- for packet in packets:
77
- # Check if packet contains CAN layer
78
- if CAN in packet:
79
- can_frame: Packet = packet[CAN]
80
-
81
- # Get timestamp
82
- if hasattr(packet, "time"):
83
- if first_timestamp is None:
84
- first_timestamp = float(packet.time)
85
- timestamp = float(packet.time) - first_timestamp
86
- else:
87
- timestamp = 0.0
88
-
89
- # Extract CAN ID and data
90
- arb_id = int(can_frame.identifier)
91
-
92
- # Get data bytes (scapy stores CAN data as bytes)
93
- if hasattr(can_frame, "data"):
94
- data = bytes(can_frame.data)
95
- else:
96
- data = b""
97
-
98
- # Determine if extended ID (bit 31 indicates extended format)
99
- # SocketCAN uses bit 31 for extended frame flag
100
- is_extended = bool(arb_id & 0x80000000)
101
- if is_extended:
102
- arb_id = arb_id & 0x1FFFFFFF # Mask to get 29-bit ID
103
-
104
- # Determine if CAN-FD (scapy may have an FD flag)
105
- is_fd = hasattr(can_frame, "flags") and (can_frame.flags & 0x01)
106
-
107
- # Extract channel if available
108
- channel = 0
109
- if hasattr(can_frame, "channel"):
110
- channel = int(can_frame.channel)
111
-
112
- # Create CANMessage
113
- can_msg = CANMessage(
114
- arbitration_id=arb_id,
115
- timestamp=timestamp,
116
- data=data,
117
- is_extended=is_extended,
118
- is_fd=is_fd,
119
- channel=channel,
120
- )
121
- messages.append(can_msg)
62
+ _extract_can_messages(packets, CAN, messages)
122
63
 
123
64
  except Exception as e:
124
65
  raise ValueError(f"Failed to parse PCAP file {path}: {e}") from e
@@ -130,3 +71,112 @@ def load_pcap(file_path: Path | str) -> CANMessageList:
130
71
  )
131
72
 
132
73
  return messages
74
+
75
+
76
+ def _import_scapy_modules() -> tuple[type, type]:
77
+ """Import and return required scapy modules.
78
+
79
+ Returns:
80
+ Tuple of (rdpcap function, CAN class).
81
+
82
+ Raises:
83
+ ImportError: If scapy is not installed.
84
+ """
85
+ try:
86
+ from scapy.all import rdpcap
87
+ from scapy.layers.can import CAN
88
+
89
+ return rdpcap, CAN # type: ignore[return-value]
90
+ except ImportError as e:
91
+ msg = "scapy library is required for PCAP loading. Install with: uv pip install scapy"
92
+ raise ImportError(msg) from e
93
+
94
+
95
+ def _extract_can_messages(
96
+ packets: list[object], CAN: type, messages: CANMessageList
97
+ ) -> float | None:
98
+ """Extract CAN messages from PCAP packets.
99
+
100
+ Args:
101
+ packets: List of scapy packets.
102
+ CAN: CAN layer class from scapy.
103
+ messages: CANMessageList to populate.
104
+
105
+ Returns:
106
+ First timestamp value for reference.
107
+ """
108
+ first_timestamp: float | None = None
109
+
110
+ for packet in packets:
111
+ if CAN in packet: # type: ignore[operator]
112
+ can_frame = packet[CAN] # type: ignore[index]
113
+ timestamp = _extract_timestamp(packet, first_timestamp)
114
+
115
+ if first_timestamp is None and hasattr(packet, "time"):
116
+ first_timestamp = float(packet.time)
117
+
118
+ can_msg = _create_can_message(can_frame, timestamp)
119
+ messages.append(can_msg)
120
+
121
+ return first_timestamp
122
+
123
+
124
+ def _extract_timestamp(packet: object, first_timestamp: float | None) -> float:
125
+ """Extract and normalize timestamp from packet.
126
+
127
+ Args:
128
+ packet: Scapy packet with time attribute.
129
+ first_timestamp: Reference timestamp for normalization.
130
+
131
+ Returns:
132
+ Normalized timestamp in seconds.
133
+ """
134
+ if hasattr(packet, "time"):
135
+ if first_timestamp is None:
136
+ return 0.0
137
+ return float(packet.time) - first_timestamp
138
+ return 0.0
139
+
140
+
141
+ def _create_can_message(can_frame: object, timestamp: float) -> CANMessage:
142
+ """Create CANMessage from scapy CAN frame.
143
+
144
+ Args:
145
+ can_frame: Scapy CAN frame object.
146
+ timestamp: Message timestamp.
147
+
148
+ Returns:
149
+ CANMessage object.
150
+ """
151
+ arb_id: int = int(can_frame.identifier) # type: ignore[attr-defined]
152
+ # Check if data attribute exists before accessing
153
+ if hasattr(can_frame, "data"):
154
+ data: bytes = bytes(can_frame.data)
155
+ else:
156
+ data = b""
157
+
158
+ # Determine if extended ID (bit 31 indicates extended format)
159
+ is_extended = bool(arb_id & 0x80000000)
160
+ if is_extended:
161
+ arb_id = arb_id & 0x1FFFFFFF # Mask to get 29-bit ID
162
+
163
+ # Determine if CAN-FD
164
+ if hasattr(can_frame, "flags"):
165
+ is_fd: bool = bool(can_frame.flags & 0x01)
166
+ else:
167
+ is_fd = False
168
+
169
+ # Extract channel if available
170
+ if hasattr(can_frame, "channel"):
171
+ channel: int = int(can_frame.channel)
172
+ else:
173
+ channel = 0
174
+
175
+ return CANMessage(
176
+ arbitration_id=arb_id,
177
+ timestamp=timestamp,
178
+ data=data,
179
+ is_extended=is_extended,
180
+ is_fd=is_fd,
181
+ channel=channel,
182
+ )
@@ -37,10 +37,14 @@ Example:
37
37
  """
38
38
 
39
39
  __all__ = [
40
+ "UDSECU",
41
+ "UDSAnalyzer",
40
42
  "UDSDecoder",
43
+ "UDSMessage",
41
44
  "UDSNegativeResponse",
42
45
  "UDSService",
43
46
  ]
44
47
 
48
+ from oscura.automotive.uds.analyzer import UDSECU, UDSAnalyzer, UDSMessage
45
49
  from oscura.automotive.uds.decoder import UDSDecoder
46
50
  from oscura.automotive.uds.models import UDSNegativeResponse, UDSService