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,605 @@
1
+ """J1939 protocol analyzer with transport protocol and SPN support.
2
+
3
+ This module provides comprehensive J1939 (SAE J1939) protocol analysis for heavy-duty
4
+ vehicles and industrial equipment, including:
5
+ - J1939 CAN identifier decoding (29-bit extended IDs)
6
+ - PGN (Parameter Group Number) extraction
7
+ - Priority and address parsing
8
+ - Transport protocol (TP.CM, TP.DT, BAM) multi-packet reassembly
9
+ - SPN (Suspect Parameter Number) decoding
10
+ - Message and topology export
11
+
12
+ Example:
13
+ >>> from oscura.automotive.j1939.analyzer import J1939Analyzer
14
+ >>> analyzer = J1939Analyzer()
15
+ >>> msg = analyzer.parse_message(0x18FEF100, b'\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07', 1.0)
16
+ >>> print(msg.identifier.pgn)
17
+ 65265
18
+ >>> print(msg.pgn_name)
19
+ Cruise Control/Vehicle Speed
20
+
21
+ References:
22
+ SAE J1939/21 - Data Link Layer
23
+ SAE J1939/71 - Vehicle Application Layer
24
+ SAE J1939/73 - Application Layer - Diagnostics
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ from dataclasses import dataclass, field
31
+ from pathlib import Path
32
+ from typing import Any, ClassVar
33
+
34
+ __all__ = [
35
+ "J1939SPN",
36
+ "J1939Analyzer",
37
+ "J1939Identifier",
38
+ "J1939Message",
39
+ ]
40
+
41
+
42
+ @dataclass
43
+ class J1939Identifier:
44
+ """J1939 CAN identifier breakdown.
45
+
46
+ Attributes:
47
+ priority: Message priority (0-7, lower is higher priority).
48
+ reserved: Reserved bit (typically 0).
49
+ data_page: Data page bit (0 or 1).
50
+ pdu_format: PDU Format (8 bits).
51
+ pdu_specific: PDU Specific (8 bits, destination or group extension).
52
+ source_address: Source address (8 bits).
53
+ pgn: Calculated Parameter Group Number.
54
+ """
55
+
56
+ priority: int
57
+ reserved: int
58
+ data_page: int
59
+ pdu_format: int
60
+ pdu_specific: int
61
+ source_address: int
62
+ pgn: int
63
+
64
+
65
+ @dataclass
66
+ class J1939Message:
67
+ """J1939 message representation.
68
+
69
+ Attributes:
70
+ timestamp: Message timestamp in seconds.
71
+ can_id: 29-bit extended CAN identifier.
72
+ identifier: Decoded J1939 identifier components.
73
+ data: Message data payload (up to 8 bytes for single frame).
74
+ pgn_name: Human-readable PGN name (if known).
75
+ decoded_spns: Decoded Suspect Parameter Numbers.
76
+ is_transport_protocol: True if transport protocol message (TP.CM/TP.DT/BAM).
77
+ transport_info: Transport protocol metadata.
78
+ """
79
+
80
+ timestamp: float
81
+ can_id: int
82
+ identifier: J1939Identifier
83
+ data: bytes
84
+ pgn_name: str | None = None
85
+ decoded_spns: dict[str, Any] = field(default_factory=dict)
86
+ is_transport_protocol: bool = False
87
+ transport_info: dict[str, Any] | None = None
88
+
89
+
90
+ @dataclass
91
+ class J1939SPN:
92
+ """Suspect Parameter Number definition.
93
+
94
+ Attributes:
95
+ spn: SPN number.
96
+ name: Parameter name.
97
+ start_bit: Bit position in data (0-based).
98
+ bit_length: Number of bits.
99
+ resolution: Scaling factor.
100
+ offset: Offset to add after scaling.
101
+ unit: Engineering unit.
102
+ data_range: Valid data range (min, max).
103
+ """
104
+
105
+ spn: int
106
+ name: str
107
+ start_bit: int
108
+ bit_length: int
109
+ resolution: float = 1.0
110
+ offset: float = 0.0
111
+ unit: str = ""
112
+ data_range: tuple[float, float] = (0.0, 0.0)
113
+
114
+
115
+ class J1939Analyzer:
116
+ """J1939 protocol analyzer for heavy-duty vehicles.
117
+
118
+ Supports comprehensive J1939 protocol analysis including:
119
+ - 29-bit extended CAN ID decoding
120
+ - PGN extraction and naming
121
+ - Transport protocol (TP.CM, TP.DT, BAM) multi-packet reassembly
122
+ - SPN decoding with user-defined parameters
123
+ - Message history and export
124
+
125
+ Example:
126
+ >>> analyzer = J1939Analyzer()
127
+ >>> # Parse a single-frame message
128
+ >>> msg = analyzer.parse_message(0x0CF00400, b'\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff')
129
+ >>> print(f"PGN: {msg.identifier.pgn}, Priority: {msg.identifier.priority}")
130
+ PGN: 61444, Priority: 3
131
+ >>> # Add SPN definition
132
+ >>> spn = J1939SPN(
133
+ ... spn=190,
134
+ ... name="Engine Speed",
135
+ ... start_bit=24,
136
+ ... bit_length=16,
137
+ ... resolution=0.125,
138
+ ... unit="rpm"
139
+ ... )
140
+ >>> analyzer.add_spn_definition(61444, spn)
141
+ """
142
+
143
+ # Well-known PGNs per SAE J1939/71
144
+ PGNS: ClassVar[dict[int, str]] = {
145
+ 0: "Torque/Speed Control 1",
146
+ 59392: "Acknowledgment",
147
+ 59904: "Request",
148
+ 60160: "Transport Protocol - Connection Management (TP.CM)",
149
+ 60416: "Transport Protocol - Data Transfer (TP.DT)",
150
+ 60928: "Address Claimed",
151
+ 61440: "Electronic Retarder Controller 1",
152
+ 61441: "Electronic Brake Controller 1",
153
+ 61442: "Electronic Transmission Controller 1",
154
+ 61443: "Electronic Engine Controller 2",
155
+ 61444: "Electronic Engine Controller 1",
156
+ 65226: "Active Diagnostic Trouble Codes",
157
+ 65227: "Previously Active Diagnostic Trouble Codes",
158
+ 65265: "Cruise Control/Vehicle Speed",
159
+ }
160
+
161
+ # Transport Protocol Connection Management control bytes
162
+ TP_CM_RTS: ClassVar[int] = 16 # Request To Send
163
+ TP_CM_CTS: ClassVar[int] = 17 # Clear To Send
164
+ TP_CM_EOM_ACK: ClassVar[int] = 19 # End of Message Acknowledgment
165
+ TP_CM_BAM: ClassVar[int] = 32 # Broadcast Announce Message
166
+ TP_CM_ABORT: ClassVar[int] = 255 # Connection Abort
167
+
168
+ def __init__(self) -> None:
169
+ """Initialize J1939 analyzer."""
170
+ self.messages: list[J1939Message] = []
171
+ # Transport sessions: (source_addr, dest_addr) -> session dict
172
+ self.transport_sessions: dict[tuple[int, int], dict[str, Any]] = {}
173
+ # SPN definitions: PGN -> [SPNs]
174
+ self.spn_definitions: dict[int, list[J1939SPN]] = {}
175
+
176
+ def parse_message(self, can_id: int, data: bytes, timestamp: float = 0.0) -> J1939Message:
177
+ """Parse J1939 message from CAN frame.
178
+
179
+ Args:
180
+ can_id: 29-bit extended CAN identifier.
181
+ data: Message data payload (up to 8 bytes).
182
+ timestamp: Message timestamp in seconds.
183
+
184
+ Returns:
185
+ Parsed J1939 message.
186
+
187
+ Raises:
188
+ ValueError: If CAN ID is invalid or data is too long.
189
+
190
+ Example:
191
+ >>> analyzer = J1939Analyzer()
192
+ >>> msg = analyzer.parse_message(0x18FEF100, b'\\x00\\x01\\x02\\x03')
193
+ >>> msg.identifier.pgn
194
+ 65265
195
+ """
196
+ if can_id > 0x1FFFFFFF:
197
+ raise ValueError(f"Invalid 29-bit CAN ID: 0x{can_id:08X}")
198
+ if len(data) > 8:
199
+ raise ValueError(f"Data too long: {len(data)} bytes (max 8)")
200
+
201
+ # Decode identifier
202
+ identifier = self._decode_identifier(can_id)
203
+
204
+ # Get PGN name
205
+ pgn_name = self.PGNS.get(identifier.pgn)
206
+
207
+ # Parse transport protocol
208
+ transport_info = self._parse_transport_protocol(identifier.pgn, data)
209
+ is_transport = transport_info is not None
210
+
211
+ # Handle multi-packet transport data
212
+ if is_transport and transport_info is not None:
213
+ tp_type = transport_info.get("type")
214
+ if tp_type == "TP.DT":
215
+ # Data transfer packet - might complete a session
216
+ self._handle_transport_data(
217
+ identifier.source_address,
218
+ identifier.pdu_specific
219
+ if self._is_pdu1_format(identifier.pdu_format)
220
+ else 0xFF,
221
+ data,
222
+ timestamp,
223
+ )
224
+
225
+ # Decode SPNs if not transport protocol
226
+ decoded_spns: dict[str, Any] = {}
227
+ if not is_transport:
228
+ decoded_spns = self._decode_spns(identifier.pgn, data)
229
+
230
+ # Create message
231
+ msg = J1939Message(
232
+ timestamp=timestamp,
233
+ can_id=can_id,
234
+ identifier=identifier,
235
+ data=data,
236
+ pgn_name=pgn_name,
237
+ decoded_spns=decoded_spns,
238
+ is_transport_protocol=is_transport,
239
+ transport_info=transport_info,
240
+ )
241
+
242
+ self.messages.append(msg)
243
+ return msg
244
+
245
+ def _decode_identifier(self, can_id: int) -> J1939Identifier:
246
+ """Decode 29-bit J1939 identifier into components.
247
+
248
+ J1939 Identifier Format (bits 28-0):
249
+ - Priority (bits 26-28): 3 bits
250
+ - Reserved (bit 25): 1 bit
251
+ - Data Page (bit 24): 1 bit
252
+ - PDU Format (bits 16-23): 8 bits
253
+ - PDU Specific (bits 8-15): 8 bits (destination or group extension)
254
+ - Source Address (bits 0-7): 8 bits
255
+
256
+ Args:
257
+ can_id: 29-bit extended CAN identifier.
258
+
259
+ Returns:
260
+ Decoded identifier components.
261
+
262
+ Example:
263
+ >>> analyzer = J1939Analyzer()
264
+ >>> ident = analyzer._decode_identifier(0x18FEF100)
265
+ >>> ident.priority
266
+ 6
267
+ >>> ident.pgn
268
+ 65265
269
+ """
270
+ priority = (can_id >> 26) & 0x07
271
+ reserved = (can_id >> 25) & 0x01
272
+ data_page = (can_id >> 24) & 0x01
273
+ pdu_format = (can_id >> 16) & 0xFF
274
+ pdu_specific = (can_id >> 8) & 0xFF
275
+ source_address = can_id & 0xFF
276
+
277
+ # Calculate PGN
278
+ pgn = self._calculate_pgn(pdu_format, pdu_specific, data_page)
279
+
280
+ return J1939Identifier(
281
+ priority=priority,
282
+ reserved=reserved,
283
+ data_page=data_page,
284
+ pdu_format=pdu_format,
285
+ pdu_specific=pdu_specific,
286
+ source_address=source_address,
287
+ pgn=pgn,
288
+ )
289
+
290
+ def _calculate_pgn(self, pdu_format: int, pdu_specific: int, data_page: int) -> int:
291
+ """Calculate PGN (Parameter Group Number).
292
+
293
+ PGN Calculation:
294
+ - If PDU1 format (PDU Format < 240): PGN = DP | PF | 00
295
+ - If PDU2 format (PDU Format >= 240): PGN = DP | PF | PS
296
+
297
+ Where:
298
+ - DP = Data Page (bit 24)
299
+ - PF = PDU Format (bits 16-23)
300
+ - PS = PDU Specific (bits 8-15)
301
+
302
+ Args:
303
+ pdu_format: PDU Format byte.
304
+ pdu_specific: PDU Specific byte.
305
+ data_page: Data Page bit.
306
+
307
+ Returns:
308
+ Calculated PGN.
309
+
310
+ Example:
311
+ >>> analyzer = J1939Analyzer()
312
+ >>> analyzer._calculate_pgn(0xFE, 0xF1, 0)
313
+ 65265
314
+ """
315
+ if self._is_pdu1_format(pdu_format):
316
+ # PDU1: PDU Specific is destination address, set to 0 for PGN
317
+ pgn = (data_page << 16) | (pdu_format << 8) | 0x00
318
+ else:
319
+ # PDU2: PDU Specific is group extension
320
+ pgn = (data_page << 16) | (pdu_format << 8) | pdu_specific
321
+
322
+ return pgn
323
+
324
+ def _is_pdu1_format(self, pdu_format: int) -> bool:
325
+ """Check if PDU1 format (destination-specific).
326
+
327
+ PDU1 format if PDU Format < 240 (0xF0), meaning PDU Specific
328
+ field contains destination address.
329
+
330
+ Args:
331
+ pdu_format: PDU Format byte.
332
+
333
+ Returns:
334
+ True if PDU1 format.
335
+
336
+ Example:
337
+ >>> analyzer = J1939Analyzer()
338
+ >>> analyzer._is_pdu1_format(0xEF)
339
+ True
340
+ >>> analyzer._is_pdu1_format(0xF0)
341
+ False
342
+ """
343
+ return pdu_format < 240
344
+
345
+ def _parse_transport_protocol(self, pgn: int, data: bytes) -> dict[str, Any] | None:
346
+ """Parse transport protocol (TP.CM, TP.DT, BAM).
347
+
348
+ TP.CM (PGN 60160) format:
349
+ - Byte 0: Control byte (RTS=16, CTS=17, EOM_ACK=19, BAM=32, ABORT=255)
350
+ - Byte 1-2: Total message size (little-endian)
351
+ - Byte 3: Total packets
352
+ - Byte 4: Max packets (CTS) or Reserved (RTS/BAM)
353
+ - Byte 5-7: PGN of data (little-endian, 3 bytes)
354
+
355
+ TP.DT (PGN 60416) format:
356
+ - Byte 0: Sequence number (1-255)
357
+ - Byte 1-7: Data (7 bytes per packet)
358
+
359
+ Args:
360
+ pgn: Parameter Group Number.
361
+ data: Message data.
362
+
363
+ Returns:
364
+ Transport protocol metadata or None if not transport.
365
+
366
+ Example:
367
+ >>> analyzer = J1939Analyzer()
368
+ >>> # TP.CM RTS
369
+ >>> info = analyzer._parse_transport_protocol(
370
+ ... 60160,
371
+ ... b'\\x10\\x20\\x00\\x05\\xff\\xf0\\x04\\x00'
372
+ ... )
373
+ >>> info['control']
374
+ 'RTS'
375
+ >>> info['total_size']
376
+ 32
377
+ """
378
+ if pgn == 60160: # TP.CM
379
+ if len(data) < 8:
380
+ return None
381
+
382
+ control = data[0]
383
+ total_size = int.from_bytes(data[1:3], "little")
384
+ total_packets = data[3]
385
+ max_packets = data[4] if control == self.TP_CM_CTS else 0xFF
386
+ data_pgn = int.from_bytes(data[5:8], "little")
387
+
388
+ cm_types = {
389
+ 16: "RTS",
390
+ 17: "CTS",
391
+ 19: "EOM_ACK",
392
+ 32: "BAM",
393
+ 255: "ABORT",
394
+ }
395
+
396
+ return {
397
+ "type": "TP.CM",
398
+ "control": cm_types.get(control, f"Unknown ({control})"),
399
+ "total_size": total_size,
400
+ "total_packets": total_packets,
401
+ "max_packets": max_packets if control == self.TP_CM_CTS else None,
402
+ "data_pgn": data_pgn,
403
+ }
404
+
405
+ elif pgn == 60416: # TP.DT
406
+ if len(data) < 1:
407
+ return None
408
+
409
+ sequence = data[0]
410
+ packet_data = data[1:]
411
+
412
+ return {
413
+ "type": "TP.DT",
414
+ "sequence": sequence,
415
+ "data": packet_data.hex(),
416
+ }
417
+
418
+ return None
419
+
420
+ def _handle_transport_data(
421
+ self, source_address: int, dest_address: int, data: bytes, timestamp: float
422
+ ) -> bytes | None:
423
+ """Handle multi-packet transport protocol data transfer.
424
+
425
+ Reassembles TP.DT packets into complete messages based on TP.CM
426
+ connection management.
427
+
428
+ Args:
429
+ source_address: Source address.
430
+ dest_address: Destination address.
431
+ data: TP.DT packet data.
432
+ timestamp: Packet timestamp.
433
+
434
+ Returns:
435
+ Complete reassembled message if session complete, None otherwise.
436
+
437
+ Example:
438
+ >>> analyzer = J1939Analyzer()
439
+ >>> # Setup session first with TP.CM
440
+ >>> analyzer.parse_message(
441
+ ... 0x18ECF100,
442
+ ... b'\\x10\\x0e\\x00\\x02\\xff\\xf0\\x04\\x00',
443
+ ... 1.0
444
+ ... )
445
+ <...>
446
+ >>> # Send TP.DT packets
447
+ >>> analyzer.parse_message(
448
+ ... 0x18EBF100,
449
+ ... b'\\x01\\x00\\x01\\x02\\x03\\x04\\x05\\x06',
450
+ ... 1.1
451
+ ... )
452
+ <...>
453
+ """
454
+ session_key = (source_address, dest_address)
455
+
456
+ # Extract sequence number and packet data
457
+ if len(data) < 1:
458
+ return None
459
+
460
+ sequence = data[0]
461
+ packet_data = data[1:]
462
+
463
+ # Check if session exists
464
+ if session_key not in self.transport_sessions:
465
+ # No active session - ignore
466
+ return None
467
+
468
+ session = self.transport_sessions[session_key]
469
+
470
+ # Append packet data
471
+ if "packets" not in session:
472
+ session["packets"] = {}
473
+
474
+ session["packets"][sequence] = packet_data
475
+ session["last_timestamp"] = timestamp
476
+
477
+ # Check if complete
478
+ expected_packets = session.get("total_packets", 0)
479
+ if len(session["packets"]) == expected_packets:
480
+ # Reassemble in sequence order
481
+ complete_data = b"".join(session["packets"][i] for i in range(1, expected_packets + 1))
482
+
483
+ # Trim to actual size
484
+ total_size = session.get("total_size", len(complete_data))
485
+ complete_data = complete_data[:total_size]
486
+
487
+ # Clean up session
488
+ del self.transport_sessions[session_key]
489
+
490
+ return complete_data
491
+
492
+ return None
493
+
494
+ def _decode_spns(self, pgn: int, data: bytes) -> dict[str, Any]:
495
+ """Decode Suspect Parameter Numbers from PGN data.
496
+
497
+ Args:
498
+ pgn: Parameter Group Number.
499
+ data: Message data.
500
+
501
+ Returns:
502
+ Dictionary of decoded SPN values.
503
+
504
+ Example:
505
+ >>> analyzer = J1939Analyzer()
506
+ >>> spn = J1939SPN(
507
+ ... spn=190,
508
+ ... name="Engine Speed",
509
+ ... start_bit=24,
510
+ ... bit_length=16,
511
+ ... resolution=0.125,
512
+ ... unit="rpm"
513
+ ... )
514
+ >>> analyzer.add_spn_definition(61444, spn)
515
+ >>> # Data with engine speed value
516
+ >>> decoded = analyzer._decode_spns(61444, b'\\x00\\x00\\x00\\x00\\x10\\x00\\x00\\x00')
517
+ >>> decoded.get('Engine Speed')
518
+ 2.0
519
+ """
520
+ decoded: dict[str, Any] = {}
521
+
522
+ if pgn not in self.spn_definitions:
523
+ return decoded
524
+
525
+ # Convert data to bit array
526
+ data_bits = int.from_bytes(data, "little")
527
+
528
+ for spn in self.spn_definitions[pgn]:
529
+ # Extract bits
530
+ mask = (1 << spn.bit_length) - 1
531
+ raw_value = (data_bits >> spn.start_bit) & mask
532
+
533
+ # Apply scaling
534
+ scaled_value = (raw_value * spn.resolution) + spn.offset
535
+
536
+ # Store with name
537
+ decoded[spn.name] = scaled_value
538
+
539
+ return decoded
540
+
541
+ def add_spn_definition(self, pgn: int, spn: J1939SPN) -> None:
542
+ """Add SPN definition for decoding.
543
+
544
+ Args:
545
+ pgn: Parameter Group Number.
546
+ spn: SPN definition.
547
+
548
+ Example:
549
+ >>> analyzer = J1939Analyzer()
550
+ >>> spn = J1939SPN(
551
+ ... spn=190,
552
+ ... name="Engine Speed",
553
+ ... start_bit=24,
554
+ ... bit_length=16,
555
+ ... resolution=0.125,
556
+ ... offset=0.0,
557
+ ... unit="rpm",
558
+ ... data_range=(0.0, 8031.875)
559
+ ... )
560
+ >>> analyzer.add_spn_definition(61444, spn)
561
+ """
562
+ if pgn not in self.spn_definitions:
563
+ self.spn_definitions[pgn] = []
564
+
565
+ self.spn_definitions[pgn].append(spn)
566
+
567
+ def export_messages(self, output_path: Path) -> None:
568
+ """Export parsed messages as JSON.
569
+
570
+ Args:
571
+ output_path: Path to output JSON file.
572
+
573
+ Example:
574
+ >>> analyzer = J1939Analyzer()
575
+ >>> analyzer.parse_message(0x18FEF100, b'\\x00\\x01\\x02\\x03')
576
+ <...>
577
+ >>> analyzer.export_messages(Path("j1939_messages.json"))
578
+ """
579
+ messages_data = []
580
+ for msg in self.messages:
581
+ msg_dict = {
582
+ "timestamp": msg.timestamp,
583
+ "can_id": f"0x{msg.can_id:08X}",
584
+ "pgn": msg.identifier.pgn,
585
+ "pgn_name": msg.pgn_name,
586
+ "priority": msg.identifier.priority,
587
+ "source_address": msg.identifier.source_address,
588
+ "destination_address": (
589
+ msg.identifier.pdu_specific
590
+ if self._is_pdu1_format(msg.identifier.pdu_format)
591
+ else 0xFF
592
+ ),
593
+ "data": msg.data.hex(),
594
+ "decoded_spns": msg.decoded_spns,
595
+ "is_transport_protocol": msg.is_transport_protocol,
596
+ "transport_info": msg.transport_info,
597
+ }
598
+ messages_data.append(msg_dict)
599
+
600
+ with output_path.open("w") as f:
601
+ json.dump(
602
+ {"messages": messages_data, "total_messages": len(messages_data)},
603
+ f,
604
+ indent=2,
605
+ )