oscura 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

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