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
@@ -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