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
@@ -0,0 +1,725 @@
1
+ """UDS (Unified Diagnostic Services) protocol analyzer per ISO 14229.
2
+
3
+ This module provides comprehensive UDS protocol analysis for automotive diagnostics,
4
+ supporting all standard UDS services, diagnostic sessions, security access, DTC parsing,
5
+ and ECU capability discovery.
6
+
7
+ Example:
8
+ >>> from oscura.automotive.uds.analyzer import UDSAnalyzer
9
+ >>> analyzer = UDSAnalyzer()
10
+ >>> msg = analyzer.parse_message(bytes([0x10, 0x03]), timestamp=1.0)
11
+ >>> print(msg.service_name)
12
+ DiagnosticSessionControl
13
+ >>> analyzer.export_session_flows(Path("session_flows.json"))
14
+
15
+ References:
16
+ ISO 14229-1:2020 - UDS specification
17
+ ISO 14229-2:2013 - Session layer services
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ from dataclasses import dataclass, field
24
+ from pathlib import Path
25
+ from typing import Any, ClassVar
26
+
27
+ __all__ = [
28
+ "UDSECU",
29
+ "UDSAnalyzer",
30
+ "UDSMessage",
31
+ ]
32
+
33
+
34
+ @dataclass
35
+ class UDSMessage:
36
+ """UDS message representation.
37
+
38
+ Attributes:
39
+ timestamp: Message timestamp in seconds.
40
+ service_id: Service ID (0x10-0xFF).
41
+ service_name: Human-readable service name.
42
+ is_response: True if response, False if request.
43
+ sub_function: Sub-function byte (if applicable).
44
+ data: Service data payload.
45
+ negative_response_code: NRC for negative responses.
46
+ decoded: Service-specific decoded fields.
47
+ """
48
+
49
+ timestamp: float
50
+ service_id: int
51
+ service_name: str
52
+ is_response: bool
53
+ sub_function: int | None = None
54
+ data: bytes = b""
55
+ negative_response_code: int | None = None
56
+ decoded: dict[str, Any] = field(default_factory=dict)
57
+
58
+ def __repr__(self) -> str:
59
+ """Human-readable representation."""
60
+ msg_type = "Response" if self.is_response else "Request"
61
+ nrc = f" NRC=0x{self.negative_response_code:02X}" if self.negative_response_code else ""
62
+ subfunc = f" sub=0x{self.sub_function:02X}" if self.sub_function is not None else ""
63
+ return f"UDSMessage(0x{self.service_id:02X} {self.service_name} [{msg_type}]{subfunc}{nrc})"
64
+
65
+
66
+ @dataclass
67
+ class UDSECU:
68
+ """UDS ECU information.
69
+
70
+ Attributes:
71
+ ecu_id: ECU identifier.
72
+ supported_services: Set of supported service IDs.
73
+ current_session: Current diagnostic session type.
74
+ security_level: Current security access level (0 = locked).
75
+ dtcs: List of Diagnostic Trouble Codes.
76
+ data_identifiers: Data identifier values.
77
+ """
78
+
79
+ ecu_id: str
80
+ supported_services: set[int] = field(default_factory=set)
81
+ current_session: int = 0x01 # Default session
82
+ security_level: int = 0 # Locked
83
+ dtcs: list[dict[str, Any]] = field(default_factory=list)
84
+ data_identifiers: dict[int, bytes] = field(default_factory=dict)
85
+
86
+
87
+ class UDSAnalyzer:
88
+ """UDS (Unified Diagnostic Services) protocol analyzer.
89
+
90
+ Supports comprehensive UDS protocol analysis including:
91
+ - All standard UDS services (0x10-0x3E, 0x83-0x87)
92
+ - Positive and negative responses
93
+ - Diagnostic session management
94
+ - Security access (seed/key exchange)
95
+ - DTC parsing and analysis
96
+ - Data identifier read/write
97
+ - Routine control
98
+ - Session flow export
99
+
100
+ Example:
101
+ >>> analyzer = UDSAnalyzer()
102
+ >>> # Parse a diagnostic session control request
103
+ >>> msg = analyzer.parse_message(
104
+ ... bytes([0x10, 0x03]),
105
+ ... timestamp=1.0,
106
+ ... ecu_id="ECU1"
107
+ ... )
108
+ >>> print(msg.service_name)
109
+ DiagnosticSessionControl
110
+ >>> print(msg.decoded["session_type"])
111
+ ExtendedDiagnosticSession
112
+ """
113
+
114
+ # Service IDs per ISO 14229-1
115
+ SERVICES: ClassVar[dict[int, str]] = {
116
+ 0x10: "DiagnosticSessionControl",
117
+ 0x11: "ECUReset",
118
+ 0x14: "ClearDiagnosticInformation",
119
+ 0x19: "ReadDTCInformation",
120
+ 0x22: "ReadDataByIdentifier",
121
+ 0x23: "ReadMemoryByAddress",
122
+ 0x24: "ReadScalingDataByIdentifier",
123
+ 0x27: "SecurityAccess",
124
+ 0x28: "CommunicationControl",
125
+ 0x2A: "ReadDataByPeriodicIdentifier",
126
+ 0x2C: "DynamicallyDefineDataIdentifier",
127
+ 0x2E: "WriteDataByIdentifier",
128
+ 0x2F: "InputOutputControlByIdentifier",
129
+ 0x31: "RoutineControl",
130
+ 0x34: "RequestDownload",
131
+ 0x35: "RequestUpload",
132
+ 0x36: "TransferData",
133
+ 0x37: "RequestTransferExit",
134
+ 0x38: "RequestFileTransfer",
135
+ 0x3D: "WriteMemoryByAddress",
136
+ 0x3E: "TesterPresent",
137
+ 0x83: "AccessTimingParameter",
138
+ 0x84: "SecuredDataTransmission",
139
+ 0x85: "ControlDTCSetting",
140
+ 0x86: "ResponseOnEvent",
141
+ 0x87: "LinkControl",
142
+ }
143
+
144
+ # Diagnostic sessions
145
+ DIAGNOSTIC_SESSIONS: ClassVar[dict[int, str]] = {
146
+ 0x01: "DefaultSession",
147
+ 0x02: "ProgrammingSession",
148
+ 0x03: "ExtendedDiagnosticSession",
149
+ 0x04: "SafetySystemDiagnosticSession",
150
+ }
151
+
152
+ # Negative response codes per ISO 14229-1
153
+ NEGATIVE_RESPONSE_CODES: ClassVar[dict[int, str]] = {
154
+ 0x10: "GeneralReject",
155
+ 0x11: "ServiceNotSupported",
156
+ 0x12: "SubFunctionNotSupported",
157
+ 0x13: "IncorrectMessageLengthOrInvalidFormat",
158
+ 0x14: "ResponseTooLong",
159
+ 0x21: "BusyRepeatRequest",
160
+ 0x22: "ConditionsNotCorrect",
161
+ 0x24: "RequestSequenceError",
162
+ 0x25: "NoResponseFromSubnetComponent",
163
+ 0x26: "FailurePreventsExecutionOfRequestedAction",
164
+ 0x31: "RequestOutOfRange",
165
+ 0x33: "SecurityAccessDenied",
166
+ 0x35: "InvalidKey",
167
+ 0x36: "ExceedNumberOfAttempts",
168
+ 0x37: "RequiredTimeDelayNotExpired",
169
+ 0x70: "UploadDownloadNotAccepted",
170
+ 0x71: "TransferDataSuspended",
171
+ 0x72: "GeneralProgrammingFailure",
172
+ 0x73: "WrongBlockSequenceCounter",
173
+ 0x78: "RequestCorrectlyReceived-ResponsePending",
174
+ 0x7E: "SubFunctionNotSupportedInActiveSession",
175
+ 0x7F: "ServiceNotSupportedInActiveSession",
176
+ }
177
+
178
+ def __init__(self) -> None:
179
+ """Initialize UDS analyzer."""
180
+ self.messages: list[UDSMessage] = []
181
+ self.ecus: dict[str, UDSECU] = {}
182
+
183
+ def parse_message(
184
+ self, data: bytes, timestamp: float = 0.0, ecu_id: str = "ECU1"
185
+ ) -> UDSMessage:
186
+ """Parse UDS message (CAN/DoIP payload).
187
+
188
+ UDS Message Format:
189
+ - Service ID (1 byte) - or 0x7F for negative response
190
+ - For negative response:
191
+ - Failed Service ID (1 byte)
192
+ - Negative Response Code (1 byte)
193
+ - For positive response:
194
+ - Service ID + 0x40
195
+ - Service-specific data
196
+ - For request:
197
+ - Service ID
198
+ - Sub-function (optional, 1 byte)
199
+ - Service-specific data
200
+
201
+ Args:
202
+ data: Raw UDS message data.
203
+ timestamp: Message timestamp in seconds.
204
+ ecu_id: ECU identifier.
205
+
206
+ Returns:
207
+ Parsed UDSMessage object.
208
+
209
+ Raises:
210
+ ValueError: If message is invalid or empty.
211
+
212
+ Example:
213
+ >>> analyzer = UDSAnalyzer()
214
+ >>> msg = analyzer.parse_message(bytes([0x10, 0x03]), timestamp=1.0)
215
+ >>> print(msg.service_name)
216
+ DiagnosticSessionControl
217
+ """
218
+ if len(data) == 0:
219
+ raise ValueError("UDS message is empty")
220
+
221
+ # Ensure ECU exists
222
+ if ecu_id not in self.ecus:
223
+ self.ecus[ecu_id] = UDSECU(ecu_id=ecu_id)
224
+
225
+ sid = data[0]
226
+
227
+ # Check for negative response
228
+ if sid == 0x7F:
229
+ if len(data) < 3:
230
+ raise ValueError("Negative response too short")
231
+ failed_sid = data[1]
232
+ nrc = data[2]
233
+
234
+ decoded = {"nrc_name": self.NEGATIVE_RESPONSE_CODES.get(nrc, "Unknown")}
235
+
236
+ msg = UDSMessage(
237
+ timestamp=timestamp,
238
+ service_id=failed_sid,
239
+ service_name=self.SERVICES.get(failed_sid, f"Unknown (0x{failed_sid:02X})"),
240
+ is_response=True,
241
+ negative_response_code=nrc,
242
+ data=data[3:],
243
+ decoded=decoded,
244
+ )
245
+ else:
246
+ # Check for positive response (SID + 0x40)
247
+ is_response = bool(sid & 0x40)
248
+ actual_sid = sid & 0xBF if is_response else sid
249
+
250
+ # Parse sub-function and data
251
+ service_data = data[1:]
252
+
253
+ decoded = self._decode_service(actual_sid, service_data, is_response)
254
+ sub_function_val = decoded.get("sub_function")
255
+ # Ensure sub_function is int or None (mypy strict)
256
+ sub_function: int | None = (
257
+ int(sub_function_val) if isinstance(sub_function_val, int) else None
258
+ )
259
+
260
+ msg = UDSMessage(
261
+ timestamp=timestamp,
262
+ service_id=actual_sid,
263
+ service_name=self.SERVICES.get(actual_sid, f"Unknown (0x{actual_sid:02X})"),
264
+ is_response=is_response,
265
+ sub_function=sub_function,
266
+ data=service_data,
267
+ decoded=decoded,
268
+ )
269
+
270
+ # Update ECU state
271
+ self._update_ecu_state(ecu_id, msg)
272
+
273
+ self.messages.append(msg)
274
+ return msg
275
+
276
+ def _decode_service(self, service_id: int, data: bytes, is_response: bool) -> dict[str, Any]:
277
+ """Decode service-specific data.
278
+
279
+ Args:
280
+ service_id: Service ID.
281
+ data: Service payload data.
282
+ is_response: True if response message.
283
+
284
+ Returns:
285
+ Dictionary of decoded fields.
286
+ """
287
+ decoders = {
288
+ 0x10: self._decode_diagnostic_session_control,
289
+ 0x11: self._decode_ecu_reset,
290
+ 0x19: self._decode_read_dtc,
291
+ 0x22: self._decode_read_data_by_id,
292
+ 0x27: self._decode_security_access,
293
+ 0x2E: self._decode_write_data_by_id,
294
+ 0x31: self._decode_routine_control,
295
+ 0x3E: self._decode_tester_present,
296
+ }
297
+
298
+ decoder = decoders.get(service_id)
299
+ if decoder:
300
+ return decoder(data, is_response)
301
+
302
+ return {}
303
+
304
+ def _decode_diagnostic_session_control(self, data: bytes, is_response: bool) -> dict[str, Any]:
305
+ """Decode DiagnosticSessionControl (0x10).
306
+
307
+ Request: [sub-function]
308
+ Response: [sub-function, P2_server_max (2 bytes), P2*_server_max (2 bytes)]
309
+
310
+ Args:
311
+ data: Service payload.
312
+ is_response: True if response.
313
+
314
+ Returns:
315
+ Decoded fields dictionary.
316
+ """
317
+ if len(data) == 0:
318
+ return {}
319
+
320
+ sub_function = data[0] & 0x7F
321
+ suppress_response = bool(data[0] & 0x80)
322
+
323
+ result = {
324
+ "sub_function": sub_function,
325
+ "suppress_positive_response": suppress_response,
326
+ "session_type": self.DIAGNOSTIC_SESSIONS.get(sub_function, f"0x{sub_function:02X}"),
327
+ }
328
+
329
+ if is_response and len(data) >= 5:
330
+ # P2_server_max and P2*_server_max in milliseconds
331
+ p2_server_max = int.from_bytes(data[1:3], "big")
332
+ p2_star_server_max = int.from_bytes(data[3:5], "big")
333
+ result["p2_server_max_ms"] = p2_server_max
334
+ result["p2_star_server_max_ms"] = p2_star_server_max
335
+
336
+ return result
337
+
338
+ def _decode_ecu_reset(self, data: bytes, is_response: bool) -> dict[str, Any]:
339
+ """Decode ECUReset (0x11).
340
+
341
+ Request: [sub-function]
342
+ Response: [sub-function, power_down_time? (1 byte)]
343
+
344
+ Sub-functions:
345
+ - 0x01: hardReset
346
+ - 0x02: keyOffOnReset
347
+ - 0x03: softReset
348
+ - 0x04: enableRapidPowerShutDown
349
+ - 0x05: disableRapidPowerShutDown
350
+
351
+ Args:
352
+ data: Service payload.
353
+ is_response: True if response.
354
+
355
+ Returns:
356
+ Decoded fields dictionary.
357
+ """
358
+ if len(data) == 0:
359
+ return {}
360
+
361
+ sub_function = data[0] & 0x7F
362
+ suppress_response = bool(data[0] & 0x80)
363
+
364
+ reset_types = {
365
+ 0x01: "hardReset",
366
+ 0x02: "keyOffOnReset",
367
+ 0x03: "softReset",
368
+ 0x04: "enableRapidPowerShutDown",
369
+ 0x05: "disableRapidPowerShutDown",
370
+ }
371
+
372
+ result = {
373
+ "sub_function": sub_function,
374
+ "suppress_positive_response": suppress_response,
375
+ "reset_type": reset_types.get(sub_function, f"0x{sub_function:02X}"),
376
+ }
377
+
378
+ if is_response and len(data) >= 2:
379
+ power_down_time = data[1]
380
+ result["power_down_time_s"] = power_down_time
381
+
382
+ return result
383
+
384
+ def _decode_read_dtc(self, data: bytes, is_response: bool) -> dict[str, Any]:
385
+ """Decode ReadDTCInformation (0x19).
386
+
387
+ Sub-functions:
388
+ - 0x01: reportNumberOfDTCByStatusMask
389
+ - 0x02: reportDTCByStatusMask
390
+ - 0x04: reportDTCSnapshotIdentification
391
+ - 0x06: reportDTCExtDataRecordByDTCNumber
392
+
393
+ Response format for 0x02:
394
+ - [sub-function, availability_mask, dtc1_high, dtc1_mid, dtc1_low, status1, ...]
395
+
396
+ Args:
397
+ data: Service payload.
398
+ is_response: True if response.
399
+
400
+ Returns:
401
+ Decoded fields dictionary.
402
+ """
403
+ if len(data) == 0:
404
+ return {}
405
+
406
+ sub_function = data[0]
407
+ result = {"sub_function": sub_function}
408
+
409
+ if sub_function == 0x02 and is_response and len(data) >= 2:
410
+ # Parse DTC list
411
+ dtcs = []
412
+ offset = 2 # Skip sub-function echo and availability mask
413
+
414
+ while offset + 4 <= len(data):
415
+ dtc_bytes = data[offset : offset + 3]
416
+ status = data[offset + 3]
417
+
418
+ # DTC format: 3 bytes (6 hex digits)
419
+ dtc_value = int.from_bytes(dtc_bytes, "big")
420
+ dtc_string = f"{dtc_value:06X}"
421
+
422
+ dtcs.append(
423
+ {
424
+ "dtc": dtc_string,
425
+ "status": status,
426
+ "test_failed": bool(status & 0x01),
427
+ "test_failed_this_operation_cycle": bool(status & 0x02),
428
+ "pending": bool(status & 0x04),
429
+ "confirmed": bool(status & 0x08),
430
+ "test_not_completed_since_last_clear": bool(status & 0x10),
431
+ "test_failed_since_last_clear": bool(status & 0x20),
432
+ "test_not_completed_this_operation_cycle": bool(status & 0x40),
433
+ "warning_indicator_requested": bool(status & 0x80),
434
+ }
435
+ )
436
+
437
+ offset += 4
438
+
439
+ result["dtcs"] = dtcs # type: ignore[assignment]
440
+ result["dtc_count"] = len(dtcs)
441
+ if len(data) >= 2:
442
+ result["availability_mask"] = data[1]
443
+
444
+ return result
445
+
446
+ def _decode_read_data_by_id(self, data: bytes, is_response: bool) -> dict[str, Any]:
447
+ """Decode ReadDataByIdentifier (0x22).
448
+
449
+ Request: [did1_high, did1_low, did2_high, did2_low, ...]
450
+ Response: [did1_high, did1_low, data1..., did2_high, did2_low, data2..., ...]
451
+
452
+ Args:
453
+ data: Service payload.
454
+ is_response: True if response.
455
+
456
+ Returns:
457
+ Decoded fields dictionary.
458
+ """
459
+ if len(data) < 2:
460
+ return {}
461
+
462
+ result: dict[str, Any] = {}
463
+
464
+ if not is_response:
465
+ # Parse requested DIDs
466
+ dids = []
467
+ offset = 0
468
+ while offset + 2 <= len(data):
469
+ did = int.from_bytes(data[offset : offset + 2], "big")
470
+ dids.append(did)
471
+ offset += 2
472
+ result["requested_dids"] = dids
473
+ else:
474
+ # Parse response DID and data
475
+ # Note: Without knowing DID data lengths, we can only parse first DID
476
+ did = int.from_bytes(data[0:2], "big")
477
+ did_data = data[2:]
478
+ result["did"] = did
479
+ result["did_data"] = did_data.hex()
480
+
481
+ return result
482
+
483
+ def _decode_security_access(self, data: bytes, is_response: bool) -> dict[str, Any]:
484
+ """Decode SecurityAccess (0x27) - seed/key exchange.
485
+
486
+ Request:
487
+ - Sub-function (1 byte) - 0x01 requestSeed, 0x02 sendKey, etc.
488
+ - Data (variable) - empty for seed request, key for sendKey
489
+
490
+ Response:
491
+ - Sub-function (1 byte)
492
+ - Seed/Key data (variable)
493
+
494
+ Args:
495
+ data: Service payload.
496
+ is_response: True if response.
497
+
498
+ Returns:
499
+ Decoded fields dictionary.
500
+
501
+ Example:
502
+ >>> analyzer = UDSAnalyzer()
503
+ >>> # Request seed for level 1
504
+ >>> msg = analyzer.parse_message(bytes([0x27, 0x01]), timestamp=1.0)
505
+ >>> print(msg.decoded["access_type"])
506
+ requestSeed
507
+ >>> print(msg.decoded["security_level"])
508
+ 1
509
+ """
510
+ if len(data) == 0:
511
+ return {}
512
+
513
+ sub_function = data[0] & 0x7F # Mask suppress positive response bit
514
+ suppress_response = bool(data[0] & 0x80)
515
+ payload = data[1:]
516
+
517
+ result = {
518
+ "sub_function": sub_function,
519
+ "suppress_positive_response": suppress_response,
520
+ }
521
+
522
+ if sub_function % 2 == 1: # Odd = requestSeed
523
+ result["access_type"] = "requestSeed" # type: ignore[assignment]
524
+ result["security_level"] = (sub_function + 1) // 2
525
+ if is_response and len(payload) > 0:
526
+ result["seed"] = payload.hex() # type: ignore[assignment]
527
+ else: # Even = sendKey
528
+ result["access_type"] = "sendKey" # type: ignore[assignment]
529
+ result["security_level"] = sub_function // 2
530
+ if not is_response and len(payload) > 0:
531
+ result["key"] = payload.hex() # type: ignore[assignment]
532
+
533
+ return result
534
+
535
+ def _decode_write_data_by_id(self, data: bytes, is_response: bool) -> dict[str, Any]:
536
+ """Decode WriteDataByIdentifier (0x2E).
537
+
538
+ Request: [did_high, did_low, data...]
539
+ Response: [did_high, did_low]
540
+
541
+ Args:
542
+ data: Service payload.
543
+ is_response: True if response.
544
+
545
+ Returns:
546
+ Decoded fields dictionary.
547
+ """
548
+ if len(data) < 2:
549
+ return {}
550
+
551
+ did = int.from_bytes(data[0:2], "big")
552
+ result = {"did": did}
553
+
554
+ if not is_response and len(data) > 2:
555
+ result["did_data"] = data[2:].hex() # type: ignore[assignment]
556
+
557
+ return result
558
+
559
+ def _decode_routine_control(self, data: bytes, is_response: bool) -> dict[str, Any]:
560
+ """Decode RoutineControl (0x31).
561
+
562
+ Request: [sub-function, routine_id_high, routine_id_low, routine_option...]
563
+ Response: [sub-function, routine_id_high, routine_id_low, status_record...]
564
+
565
+ Sub-functions:
566
+ - 0x01: startRoutine
567
+ - 0x02: stopRoutine
568
+ - 0x03: requestRoutineResults
569
+
570
+ Args:
571
+ data: Service payload.
572
+ is_response: True if response.
573
+
574
+ Returns:
575
+ Decoded fields dictionary.
576
+ """
577
+ if len(data) < 3:
578
+ return {}
579
+
580
+ sub_function = data[0] & 0x7F
581
+ suppress_response = bool(data[0] & 0x80)
582
+ routine_id = int.from_bytes(data[1:3], "big")
583
+
584
+ routine_types = {
585
+ 0x01: "startRoutine",
586
+ 0x02: "stopRoutine",
587
+ 0x03: "requestRoutineResults",
588
+ }
589
+
590
+ result = {
591
+ "sub_function": sub_function,
592
+ "suppress_positive_response": suppress_response,
593
+ "routine_type": routine_types.get(sub_function, f"0x{sub_function:02X}"),
594
+ "routine_id": routine_id,
595
+ }
596
+
597
+ if len(data) > 3:
598
+ if is_response:
599
+ result["status_record"] = data[3:].hex()
600
+ else:
601
+ result["routine_option"] = data[3:].hex()
602
+
603
+ return result
604
+
605
+ def _decode_tester_present(self, data: bytes, is_response: bool) -> dict[str, Any]:
606
+ """Decode TesterPresent (0x3E).
607
+
608
+ Request: [sub-function] (typically 0x00 or 0x80)
609
+ Response: [sub-function]
610
+
611
+ Args:
612
+ data: Service payload.
613
+ is_response: True if response.
614
+
615
+ Returns:
616
+ Decoded fields dictionary.
617
+ """
618
+ if len(data) == 0:
619
+ return {}
620
+
621
+ sub_function = data[0] & 0x7F
622
+ suppress_response = bool(data[0] & 0x80)
623
+
624
+ return {
625
+ "sub_function": sub_function,
626
+ "suppress_positive_response": suppress_response,
627
+ }
628
+
629
+ def _update_ecu_state(self, ecu_id: str, msg: UDSMessage) -> None:
630
+ """Update ECU state based on message.
631
+
632
+ Args:
633
+ ecu_id: ECU identifier.
634
+ msg: Parsed UDS message.
635
+ """
636
+ ecu = self.ecus[ecu_id]
637
+
638
+ # Track supported services from requests
639
+ if not msg.is_response and msg.negative_response_code is None:
640
+ ecu.supported_services.add(msg.service_id)
641
+
642
+ # Only process successful responses
643
+ if not msg.is_response or msg.negative_response_code is not None:
644
+ return
645
+
646
+ # Update session state (0x10 DiagnosticSessionControl)
647
+ if msg.service_id == 0x10:
648
+ self._update_session_state(ecu, msg)
649
+
650
+ # Update security level (0x27 SecurityAccess)
651
+ if msg.service_id == 0x27:
652
+ self._update_security_level(ecu, msg)
653
+
654
+ # Store DTCs (0x19 ReadDTCInformation)
655
+ if msg.service_id == 0x19:
656
+ self._store_dtcs(ecu, msg)
657
+
658
+ # Store data identifiers (0x22 ReadDataByIdentifier)
659
+ if msg.service_id == 0x22:
660
+ self._store_data_identifier(ecu, msg)
661
+
662
+ def _update_session_state(self, ecu: UDSECU, msg: UDSMessage) -> None:
663
+ """Update ECU diagnostic session state."""
664
+ session_type = msg.decoded.get("sub_function")
665
+ if session_type is not None:
666
+ ecu.current_session = session_type
667
+
668
+ def _update_security_level(self, ecu: UDSECU, msg: UDSMessage) -> None:
669
+ """Update ECU security access level."""
670
+ if msg.decoded.get("access_type") == "sendKey":
671
+ level = msg.decoded.get("security_level", 0)
672
+ ecu.security_level = level
673
+
674
+ def _store_dtcs(self, ecu: UDSECU, msg: UDSMessage) -> None:
675
+ """Store diagnostic trouble codes."""
676
+ dtcs = msg.decoded.get("dtcs")
677
+ if dtcs:
678
+ ecu.dtcs = dtcs
679
+
680
+ def _store_data_identifier(self, ecu: UDSECU, msg: UDSMessage) -> None:
681
+ """Store data identifier value."""
682
+ did = msg.decoded.get("did")
683
+ did_data_hex = msg.decoded.get("did_data")
684
+ if did is not None and did_data_hex is not None:
685
+ ecu.data_identifiers[did] = bytes.fromhex(did_data_hex)
686
+
687
+ def export_session_flows(self, output_path: Path) -> None:
688
+ """Export diagnostic session flows as JSON.
689
+
690
+ Args:
691
+ output_path: Path to output JSON file.
692
+
693
+ Example:
694
+ >>> analyzer = UDSAnalyzer()
695
+ >>> # ... parse messages ...
696
+ >>> analyzer.export_session_flows(Path("flows.json"))
697
+ """
698
+ flows = {
699
+ "messages": [
700
+ {
701
+ "timestamp": msg.timestamp,
702
+ "service_id": msg.service_id,
703
+ "service_name": msg.service_name,
704
+ "is_response": msg.is_response,
705
+ "sub_function": msg.sub_function,
706
+ "negative_response_code": msg.negative_response_code,
707
+ "decoded": msg.decoded,
708
+ }
709
+ for msg in self.messages
710
+ ],
711
+ "ecus": {
712
+ ecu_id: {
713
+ "supported_services": sorted(ecu.supported_services),
714
+ "current_session": ecu.current_session,
715
+ "security_level": ecu.security_level,
716
+ "dtc_count": len(ecu.dtcs),
717
+ "dtcs": ecu.dtcs,
718
+ "data_identifier_count": len(ecu.data_identifiers),
719
+ }
720
+ for ecu_id, ecu in self.ecus.items()
721
+ },
722
+ }
723
+
724
+ with output_path.open("w") as f:
725
+ json.dump(flows, f, indent=2)