oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl

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