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,612 @@
1
+ """LIN protocol analyzer with enhanced checksum and LDF generation.
2
+
3
+ This module provides comprehensive LIN 2.x protocol analysis including
4
+ protected ID calculation, enhanced checksum validation, diagnostic frame
5
+ parsing, and LDF (LIN Description File) generation from captured traffic.
6
+
7
+ References:
8
+ LIN Specification 2.2A
9
+ ISO 17987 (LIN standard)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass, field
15
+ from pathlib import Path
16
+ from typing import Any, ClassVar
17
+
18
+ __all__ = [
19
+ "LINAnalyzer",
20
+ "LINFrame",
21
+ "LINScheduleEntry",
22
+ "LINSignal",
23
+ ]
24
+
25
+
26
+ @dataclass
27
+ class LINFrame:
28
+ """LIN frame representation.
29
+
30
+ Attributes:
31
+ timestamp: Frame timestamp in seconds.
32
+ frame_id: Frame ID (0-63, 6-bit).
33
+ data: Frame data bytes (1-8 bytes).
34
+ checksum: Received checksum byte.
35
+ checksum_valid: True if checksum is valid.
36
+ checksum_type: Checksum type ("classic" or "enhanced").
37
+ parity_bits: 2-bit parity from protected ID.
38
+ is_diagnostic: True if diagnostic frame (0x3C or 0x3D).
39
+ decoded_signals: Decoded signal values.
40
+ """
41
+
42
+ timestamp: float
43
+ frame_id: int
44
+ data: bytes
45
+ checksum: int
46
+ checksum_valid: bool
47
+ checksum_type: str = "enhanced"
48
+ parity_bits: int = 0
49
+ is_diagnostic: bool = False
50
+ decoded_signals: dict[str, Any] = field(default_factory=dict)
51
+
52
+ def __repr__(self) -> str:
53
+ """Human-readable representation."""
54
+ status = "✓" if self.checksum_valid else "✗"
55
+ return (
56
+ f"LINFrame(ID=0x{self.frame_id:02X}, t={self.timestamp:.6f}s, "
57
+ f"data={self.data.hex().upper()}, checksum={status} {self.checksum_type})"
58
+ )
59
+
60
+
61
+ @dataclass
62
+ class LINSignal:
63
+ """LIN signal definition.
64
+
65
+ Attributes:
66
+ name: Signal name.
67
+ frame_id: Frame ID containing this signal (0-63).
68
+ start_bit: Starting bit position (0-63 within 8 bytes).
69
+ bit_length: Signal length in bits (1-64).
70
+ init_value: Initial/default value.
71
+ publisher: Node publishing this signal ("Master" or slave name).
72
+ """
73
+
74
+ name: str
75
+ frame_id: int
76
+ start_bit: int
77
+ bit_length: int
78
+ init_value: int = 0
79
+ publisher: str = "Master"
80
+
81
+
82
+ @dataclass
83
+ class LINScheduleEntry:
84
+ """LIN schedule table entry.
85
+
86
+ Attributes:
87
+ frame_id: Frame ID to transmit.
88
+ delay_ms: Delay before next frame in milliseconds.
89
+ """
90
+
91
+ frame_id: int
92
+ delay_ms: float
93
+
94
+
95
+ class LINAnalyzer:
96
+ """Enhanced LIN protocol analyzer with LDF generation.
97
+
98
+ Supports comprehensive LIN 2.x protocol analysis including:
99
+ - Protected ID calculation with parity bits
100
+ - Classic and enhanced checksum validation
101
+ - Diagnostic frame parsing (0x3C master request, 0x3D slave response)
102
+ - Signal decoding with bit-level extraction
103
+ - Schedule table inference from frame timing
104
+ - LDF (LIN Description File) generation
105
+
106
+ Example:
107
+ >>> analyzer = LINAnalyzer()
108
+ >>> # Parse LIN frame with enhanced checksum
109
+ >>> frame = analyzer.parse_frame(
110
+ ... data=b'\\x55\\x80\\x01\\x02\\x03\\xFA',
111
+ ... timestamp=1.0
112
+ ... )
113
+ >>> print(f"Frame ID: {frame.frame_id}")
114
+ Frame ID: 0
115
+ >>> # Add signal definition
116
+ >>> analyzer.add_signal(LINSignal(
117
+ ... name="Speed",
118
+ ... frame_id=0,
119
+ ... start_bit=0,
120
+ ... bit_length=16,
121
+ ... publisher="Master"
122
+ ... ))
123
+ >>> # Generate LDF from captured traffic
124
+ >>> analyzer.generate_ldf(Path("output.ldf"), baudrate=19200)
125
+ """
126
+
127
+ # Diagnostic frame IDs per LIN 2.x specification
128
+ MASTER_REQUEST_FRAME: ClassVar[int] = 0x3C # 60
129
+ SLAVE_RESPONSE_FRAME: ClassVar[int] = 0x3D # 61
130
+
131
+ # Diagnostic services (subset of UDS adapted for LIN)
132
+ DIAGNOSTIC_SERVICES: ClassVar[dict[int, str]] = {
133
+ 0xB0: "AssignFrameIdRange",
134
+ 0xB1: "AssignNAD",
135
+ 0xB2: "ConditionalChangeNAD",
136
+ 0xB3: "DataDump",
137
+ 0xB4: "SaveConfiguration",
138
+ 0xB5: "AssignFrameId",
139
+ 0xB6: "ReadById",
140
+ 0xB7: "TargetedReset",
141
+ }
142
+
143
+ def __init__(self) -> None:
144
+ """Initialize LIN analyzer."""
145
+ self.frames: list[LINFrame] = []
146
+ self.signals: list[LINSignal] = []
147
+ self.schedule: list[LINScheduleEntry] = []
148
+ self.detected_frame_ids: set[int] = set()
149
+
150
+ def parse_frame(
151
+ self,
152
+ data: bytes,
153
+ timestamp: float = 0.0,
154
+ checksum_type: str = "enhanced",
155
+ ) -> LINFrame:
156
+ """Parse LIN frame including sync, protected ID, data, and checksum.
157
+
158
+ LIN Frame Format:
159
+ - Break field (dominant, >= 13 bit times) - not in data
160
+ - Sync field (0x55) - first byte
161
+ - Protected ID (frame ID + parity bits) - second byte
162
+ - Data field (1-8 bytes) - variable
163
+ - Checksum field (1 byte) - last byte
164
+
165
+ Args:
166
+ data: Raw frame bytes including sync, protected ID, data, checksum.
167
+ timestamp: Frame timestamp in seconds.
168
+ checksum_type: Checksum type ("classic" or "enhanced").
169
+
170
+ Returns:
171
+ Parsed LINFrame object.
172
+
173
+ Raises:
174
+ ValueError: If frame is invalid or too short.
175
+
176
+ Example:
177
+ >>> analyzer = LINAnalyzer()
178
+ >>> # Frame: sync=0x55, protected_id=0x80 (ID=0), data=0x01,0x02, checksum=0xFA
179
+ >>> frame = analyzer.parse_frame(
180
+ ... data=b'\\x55\\x80\\x01\\x02\\xFA',
181
+ ... timestamp=1.0,
182
+ ... checksum_type="enhanced"
183
+ ... )
184
+ >>> print(f"Frame ID: {frame.frame_id}, Valid: {frame.checksum_valid}")
185
+ Frame ID: 0, Valid: True
186
+ """
187
+ if len(data) < 4: # Minimum: sync + protected_id + 1 data byte + checksum
188
+ raise ValueError(f"LIN frame too short: {len(data)} bytes (minimum 4)")
189
+
190
+ # Parse sync byte (should be 0x55)
191
+ sync_byte = data[0]
192
+ if sync_byte != 0x55:
193
+ raise ValueError(f"Invalid sync byte: 0x{sync_byte:02X} (expected 0x55)")
194
+
195
+ # Parse protected ID
196
+ protected_id = data[1]
197
+ frame_id = protected_id & 0x3F # Lower 6 bits
198
+ parity_bits = (protected_id >> 6) & 0x03 # Upper 2 bits
199
+
200
+ # Validate protected ID parity
201
+ expected_protected_id = self._calculate_protected_id(frame_id)
202
+ if protected_id != expected_protected_id:
203
+ raise ValueError(
204
+ f"Invalid protected ID parity: 0x{protected_id:02X} "
205
+ f"(expected 0x{expected_protected_id:02X} for frame ID {frame_id})"
206
+ )
207
+
208
+ # Parse data and checksum
209
+ frame_data = data[2:-1]
210
+ received_checksum = data[-1]
211
+
212
+ # Calculate and validate checksum
213
+ if checksum_type == "classic":
214
+ expected_checksum = self._calculate_classic_checksum(frame_data)
215
+ else: # enhanced
216
+ expected_checksum = self._calculate_enhanced_checksum(protected_id, frame_data)
217
+
218
+ checksum_valid = received_checksum == expected_checksum
219
+
220
+ # Check if diagnostic frame
221
+ is_diagnostic = frame_id in (self.MASTER_REQUEST_FRAME, self.SLAVE_RESPONSE_FRAME)
222
+
223
+ # Parse diagnostic frame if applicable
224
+ decoded_signals: dict[str, Any] = {}
225
+ if is_diagnostic:
226
+ decoded_signals = self._parse_diagnostic_frame(frame_id, frame_data)
227
+ else:
228
+ # Decode regular signals
229
+ decoded_signals = self.decode_signals(frame_id, frame_data)
230
+
231
+ frame = LINFrame(
232
+ timestamp=timestamp,
233
+ frame_id=frame_id,
234
+ data=frame_data,
235
+ checksum=received_checksum,
236
+ checksum_valid=checksum_valid,
237
+ checksum_type=checksum_type,
238
+ parity_bits=parity_bits,
239
+ is_diagnostic=is_diagnostic,
240
+ decoded_signals=decoded_signals,
241
+ )
242
+
243
+ self.frames.append(frame)
244
+ self.detected_frame_ids.add(frame_id)
245
+
246
+ return frame
247
+
248
+ def _calculate_protected_id(self, frame_id: int) -> int:
249
+ """Calculate protected ID with parity bits.
250
+
251
+ Protected ID Format (8 bits):
252
+ - Bits 0-5: Frame ID (6 bits, range 0-63)
253
+ - Bit 6 (P0): ID0 XOR ID1 XOR ID2 XOR ID4
254
+ - Bit 7 (P1): NOT(ID1 XOR ID3 XOR ID4 XOR ID5)
255
+
256
+ Args:
257
+ frame_id: Frame ID (0-63).
258
+
259
+ Returns:
260
+ Protected ID with parity bits (8 bits).
261
+
262
+ Raises:
263
+ ValueError: If frame ID exceeds 63.
264
+
265
+ Example:
266
+ >>> analyzer = LINAnalyzer()
267
+ >>> protected_id = analyzer._calculate_protected_id(0)
268
+ >>> print(f"Protected ID: 0x{protected_id:02X}")
269
+ Protected ID: 0x80
270
+ """
271
+ if frame_id > 0x3F:
272
+ raise ValueError(f"Frame ID {frame_id} exceeds 6 bits (max 63)")
273
+
274
+ # Extract individual ID bits
275
+ id0 = (frame_id >> 0) & 1
276
+ id1 = (frame_id >> 1) & 1
277
+ id2 = (frame_id >> 2) & 1
278
+ id3 = (frame_id >> 3) & 1
279
+ id4 = (frame_id >> 4) & 1
280
+ id5 = (frame_id >> 5) & 1
281
+
282
+ # Calculate parity bits
283
+ p0 = id0 ^ id1 ^ id2 ^ id4
284
+ p1 = (id1 ^ id3 ^ id4 ^ id5) ^ 1 # Inverted
285
+
286
+ # Construct protected ID
287
+ protected_id = frame_id | (p0 << 6) | (p1 << 7)
288
+
289
+ return protected_id
290
+
291
+ def _calculate_classic_checksum(self, data: bytes) -> int:
292
+ """Calculate classic checksum (LIN 1.x).
293
+
294
+ Classic checksum = inverted modulo-256 sum of data bytes only.
295
+
296
+ Args:
297
+ data: Frame data bytes.
298
+
299
+ Returns:
300
+ Classic checksum byte.
301
+
302
+ Example:
303
+ >>> analyzer = LINAnalyzer()
304
+ >>> checksum = analyzer._calculate_classic_checksum(b'\\x01\\x02\\x03')
305
+ >>> print(f"Checksum: 0x{checksum:02X}")
306
+ Checksum: 0xF9
307
+ """
308
+ checksum_sum = sum(data)
309
+
310
+ # Handle carry (modulo-256 with carry propagation)
311
+ while checksum_sum > 255:
312
+ checksum_sum = (checksum_sum & 0xFF) + (checksum_sum >> 8)
313
+
314
+ checksum = (~checksum_sum) & 0xFF
315
+
316
+ return checksum
317
+
318
+ def _calculate_enhanced_checksum(self, protected_id: int, data: bytes) -> int:
319
+ """Calculate enhanced checksum (LIN 2.x).
320
+
321
+ Enhanced checksum = inverted modulo-256 sum of protected ID + data bytes.
322
+
323
+ Args:
324
+ protected_id: Protected ID byte (frame ID with parity).
325
+ data: Frame data bytes.
326
+
327
+ Returns:
328
+ Enhanced checksum byte.
329
+
330
+ Example:
331
+ >>> analyzer = LINAnalyzer()
332
+ >>> checksum = analyzer._calculate_enhanced_checksum(0x80, b'\\x01\\x02\\x03')
333
+ >>> print(f"Checksum: 0x{checksum:02X}")
334
+ Checksum: 0x79
335
+ """
336
+ checksum_sum = protected_id
337
+
338
+ for byte in data:
339
+ checksum_sum += byte
340
+ # Handle carry (modulo-256 with carry propagation)
341
+ if checksum_sum > 255:
342
+ checksum_sum = (checksum_sum & 0xFF) + 1
343
+
344
+ checksum = (~checksum_sum) & 0xFF
345
+
346
+ return checksum
347
+
348
+ def _parse_diagnostic_frame(self, frame_id: int, data: bytes) -> dict[str, Any]:
349
+ """Parse diagnostic frame (Master Request 0x3C or Slave Response 0x3D).
350
+
351
+ Diagnostic Frame Format:
352
+ - NAD (Node Address for Diagnostics) - 1 byte
353
+ - PCI (Protocol Control Information) - 1 byte
354
+ - SID (Service Identifier) - 1 byte
355
+ - Service data - variable (up to 5 bytes)
356
+
357
+ Args:
358
+ frame_id: Frame ID (0x3C or 0x3D).
359
+ data: Frame data bytes.
360
+
361
+ Returns:
362
+ Dictionary of decoded diagnostic fields.
363
+
364
+ Example:
365
+ >>> analyzer = LINAnalyzer()
366
+ >>> decoded = analyzer._parse_diagnostic_frame(
367
+ ... 0x3C,
368
+ ... b'\\x01\\x06\\xB6\\x00\\x01\\x00\\x00\\x00'
369
+ ... )
370
+ >>> print(decoded["service_name"])
371
+ ReadById
372
+ """
373
+ if len(data) < 3:
374
+ return {"error": "Diagnostic frame too short"}
375
+
376
+ nad = data[0] # Node address
377
+ pci = data[1] # Protocol control info (single frame = 0x06)
378
+ sid = data[2] # Service ID
379
+
380
+ service_name = self.DIAGNOSTIC_SERVICES.get(sid, f"Unknown (0x{sid:02X})")
381
+ service_data = data[3:]
382
+
383
+ result: dict[str, Any] = {
384
+ "nad": nad,
385
+ "pci": pci,
386
+ "service_id": sid,
387
+ "service_name": service_name,
388
+ "service_data": service_data.hex().upper(),
389
+ "frame_type": "MasterRequest"
390
+ if frame_id == self.MASTER_REQUEST_FRAME
391
+ else "SlaveResponse",
392
+ }
393
+
394
+ # Decode specific services
395
+ if sid == 0xB6 and len(service_data) >= 2: # ReadById
396
+ identifier = int.from_bytes(service_data[0:2], "big")
397
+ result["identifier"] = identifier
398
+
399
+ return result
400
+
401
+ def add_signal(self, signal: LINSignal) -> None:
402
+ """Add signal definition for decoding.
403
+
404
+ Args:
405
+ signal: Signal definition to add.
406
+
407
+ Example:
408
+ >>> analyzer = LINAnalyzer()
409
+ >>> analyzer.add_signal(LINSignal(
410
+ ... name="EngineSpeed",
411
+ ... frame_id=0x10,
412
+ ... start_bit=0,
413
+ ... bit_length=16,
414
+ ... publisher="Master"
415
+ ... ))
416
+ """
417
+ self.signals.append(signal)
418
+
419
+ def decode_signals(self, frame_id: int, data: bytes) -> dict[str, Any]:
420
+ """Decode signals from frame data.
421
+
422
+ Args:
423
+ frame_id: Frame ID.
424
+ data: Frame data bytes.
425
+
426
+ Returns:
427
+ Dictionary mapping signal names to decoded values.
428
+
429
+ Example:
430
+ >>> analyzer = LINAnalyzer()
431
+ >>> analyzer.add_signal(LINSignal("Speed", frame_id=0, start_bit=0, bit_length=16))
432
+ >>> decoded = analyzer.decode_signals(0, b'\\x10\\x27')
433
+ >>> print(f"Speed: {decoded['Speed']}")
434
+ Speed: 10000
435
+ """
436
+ decoded: dict[str, Any] = {}
437
+
438
+ # Find signals for this frame
439
+ frame_signals = [s for s in self.signals if s.frame_id == frame_id]
440
+
441
+ # Convert data to bit array
442
+ if len(data) == 0:
443
+ return decoded
444
+
445
+ data_bits = int.from_bytes(data, "little")
446
+
447
+ for signal in frame_signals:
448
+ # Extract signal bits
449
+ mask = (1 << signal.bit_length) - 1
450
+ value = (data_bits >> signal.start_bit) & mask
451
+ decoded[signal.name] = value
452
+
453
+ return decoded
454
+
455
+ def infer_schedule_table(self) -> list[LINScheduleEntry]:
456
+ """Infer schedule table from captured frame timing.
457
+
458
+ Analyzes frame timestamps to determine typical transmission schedule.
459
+
460
+ Returns:
461
+ List of schedule entries ordered by typical transmission sequence.
462
+
463
+ Example:
464
+ >>> analyzer = LINAnalyzer()
465
+ >>> # ... parse frames ...
466
+ >>> schedule = analyzer.infer_schedule_table()
467
+ >>> for entry in schedule:
468
+ ... print(f"Frame 0x{entry.frame_id:02X} delay {entry.delay_ms:.1f}ms")
469
+ """
470
+ if len(self.frames) < 2:
471
+ return []
472
+
473
+ # Group frames by ID and calculate average inter-frame delays
474
+ frame_delays: dict[int, list[float]] = {}
475
+
476
+ for i in range(1, len(self.frames)):
477
+ prev_frame = self.frames[i - 1]
478
+ curr_frame = self.frames[i]
479
+
480
+ # Calculate delay in milliseconds
481
+ delay_ms = (curr_frame.timestamp - prev_frame.timestamp) * 1000.0
482
+
483
+ # Group by current frame ID
484
+ if curr_frame.frame_id not in frame_delays:
485
+ frame_delays[curr_frame.frame_id] = []
486
+ frame_delays[curr_frame.frame_id].append(delay_ms)
487
+
488
+ # Calculate average delay for each frame ID
489
+ schedule_entries = []
490
+ for frame_id in sorted(self.detected_frame_ids):
491
+ if frame_id in frame_delays and len(frame_delays[frame_id]) > 0:
492
+ avg_delay = sum(frame_delays[frame_id]) / len(frame_delays[frame_id])
493
+ schedule_entries.append(LINScheduleEntry(frame_id=frame_id, delay_ms=avg_delay))
494
+
495
+ self.schedule = schedule_entries
496
+ return schedule_entries
497
+
498
+ def generate_ldf(self, output_path: Path, baudrate: int = 19200) -> None:
499
+ """Generate LDF (LIN Description File) from captured traffic.
500
+
501
+ LDF Format per LIN 2.x specification:
502
+ - Header (protocol version, language version, speed)
503
+ - Nodes (master and slaves)
504
+ - Signals (name, size, init value, publisher)
505
+ - Frames (ID, publisher, size, signals)
506
+ - Schedule tables (frame sequence and timing)
507
+
508
+ Args:
509
+ output_path: Path to output LDF file.
510
+ baudrate: LIN bus baudrate in bps (default 19200).
511
+
512
+ Raises:
513
+ ValueError: If no frames have been captured.
514
+
515
+ Example:
516
+ >>> analyzer = LINAnalyzer()
517
+ >>> # ... parse frames and add signals ...
518
+ >>> analyzer.generate_ldf(Path("vehicle.ldf"), baudrate=19200)
519
+ """
520
+ if len(self.frames) == 0:
521
+ raise ValueError("No frames captured - cannot generate LDF")
522
+
523
+ ldf_lines: list[str] = []
524
+ self._add_ldf_header(ldf_lines, baudrate)
525
+ self._add_ldf_nodes(ldf_lines)
526
+ self._add_ldf_signals(ldf_lines)
527
+ self._add_ldf_frames(ldf_lines)
528
+ self._add_ldf_schedule(ldf_lines)
529
+
530
+ output_path.write_text("\n".join(ldf_lines) + "\n", encoding="utf-8")
531
+
532
+ def _add_ldf_header(self, ldf_lines: list[str], baudrate: int) -> None:
533
+ """Add LDF header section."""
534
+ ldf_lines.append("LIN_description_file;")
535
+ ldf_lines.append('LIN_protocol_version = "2.1";')
536
+ ldf_lines.append('LIN_language_version = "2.1";')
537
+ ldf_lines.append(f"LIN_speed = {baudrate / 1000:.1f} kbps;")
538
+ ldf_lines.append("")
539
+
540
+ def _add_ldf_nodes(self, ldf_lines: list[str]) -> None:
541
+ """Add LDF nodes section (master and slaves)."""
542
+ ldf_lines.append("Nodes {")
543
+ ldf_lines.append(" Master: Master, 5 ms, 0.1 ms;")
544
+
545
+ slaves = sorted({s.publisher for s in self.signals if s.publisher != "Master"})
546
+ if len(slaves) > 0:
547
+ ldf_lines.append(f" Slaves: {', '.join(slaves)};")
548
+ ldf_lines.append("}")
549
+ ldf_lines.append("")
550
+
551
+ def _add_ldf_signals(self, ldf_lines: list[str]) -> None:
552
+ """Add LDF signals section."""
553
+ if len(self.signals) == 0:
554
+ return
555
+
556
+ ldf_lines.append("Signals {")
557
+ for signal in self.signals:
558
+ ldf_lines.append(
559
+ f" {signal.name}: {signal.bit_length}, {signal.init_value}, {signal.publisher};"
560
+ )
561
+ ldf_lines.append("}")
562
+ ldf_lines.append("")
563
+
564
+ def _add_ldf_frames(self, ldf_lines: list[str]) -> None:
565
+ """Add LDF frames section."""
566
+ ldf_lines.append("Frames {")
567
+ for frame_id in sorted(self.detected_frame_ids):
568
+ if frame_id in (self.MASTER_REQUEST_FRAME, self.SLAVE_RESPONSE_FRAME):
569
+ continue
570
+
571
+ frame_signals = [s for s in self.signals if s.frame_id == frame_id]
572
+ publisher, dlc = self._determine_frame_metadata(frame_id, frame_signals)
573
+
574
+ ldf_lines.append(f" Frame_{frame_id:02X}: {frame_id}, {publisher}, {dlc} {{")
575
+ if len(frame_signals) > 0:
576
+ for signal in frame_signals:
577
+ ldf_lines.append(f" {signal.name}, {signal.start_bit};")
578
+ ldf_lines.append(" }")
579
+
580
+ ldf_lines.append("}")
581
+ ldf_lines.append("")
582
+
583
+ def _determine_frame_metadata(
584
+ self, frame_id: int, frame_signals: list[LINSignal]
585
+ ) -> tuple[str, int]:
586
+ """Determine frame publisher and data length."""
587
+ if len(frame_signals) > 0:
588
+ publisher = frame_signals[0].publisher
589
+ max_bit = max((s.start_bit + s.bit_length) for s in frame_signals)
590
+ dlc = (max_bit + 7) // 8
591
+ else:
592
+ frame_data_lengths = [len(f.data) for f in self.frames if f.frame_id == frame_id]
593
+ dlc = max(frame_data_lengths) if frame_data_lengths else 1
594
+ publisher = "Master"
595
+ return publisher, dlc
596
+
597
+ def _add_ldf_schedule(self, ldf_lines: list[str]) -> None:
598
+ """Add LDF schedule tables section."""
599
+ if len(self.schedule) == 0:
600
+ self.infer_schedule_table()
601
+
602
+ if len(self.schedule) == 0:
603
+ return
604
+
605
+ ldf_lines.append("Schedule_tables {")
606
+ ldf_lines.append(" NormalTable {")
607
+ for entry in self.schedule:
608
+ if entry.frame_id in (self.MASTER_REQUEST_FRAME, self.SLAVE_RESPONSE_FRAME):
609
+ continue
610
+ ldf_lines.append(f" Frame_{entry.frame_id:02X} delay {entry.delay_ms:.1f} ms;")
611
+ ldf_lines.append(" }")
612
+ ldf_lines.append("}")
@@ -32,7 +32,7 @@ def load_blf(file_path: Path | str) -> CANMessageList:
32
32
  >>> print(f"Unique IDs: {len(messages.unique_ids())}")
33
33
  """
34
34
  try:
35
- import can # type: ignore[import-untyped]
35
+ import can
36
36
  except ImportError as e:
37
37
  raise ImportError(
38
38
  "python-can is required for BLF file support. "
@@ -56,8 +56,19 @@ def load_blf(file_path: Path | str) -> CANMessageList:
56
56
  ch = msg.channel
57
57
  if isinstance(ch, int):
58
58
  channel = ch
59
+ elif isinstance(ch, str):
60
+ channel = int(ch)
59
61
  elif ch is not None:
60
- channel = int(ch) # type: ignore[arg-type]
62
+ # Handle other types (e.g., sequences), default to 0 if conversion fails
63
+ try:
64
+ # Convert sequences to single value
65
+ from collections.abc import Sequence as ABCSequence
66
+
67
+ if isinstance(ch, ABCSequence):
68
+ channel = int(ch[0]) if ch else 0
69
+ # Note: other cases caught by exception handler
70
+ except (TypeError, ValueError, IndexError):
71
+ channel = 0
61
72
 
62
73
  can_msg = CANMessage(
63
74
  arbitration_id=msg.arbitration_id,