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
@@ -90,19 +90,19 @@ class JTAGDecoder(SyncDecoder):
90
90
  longname = "Joint Test Action Group (IEEE 1149.1)"
91
91
  desc = "JTAG/Boundary-Scan protocol decoder"
92
92
 
93
- channels = [ # noqa: RUF012
93
+ channels = [
94
94
  ChannelDef("tck", "TCK", "Test Clock", required=True),
95
95
  ChannelDef("tms", "TMS", "Test Mode Select", required=True),
96
96
  ChannelDef("tdi", "TDI", "Test Data In", required=True),
97
97
  ]
98
98
 
99
- optional_channels = [ # noqa: RUF012
99
+ optional_channels = [
100
100
  ChannelDef("tdo", "TDO", "Test Data Out", required=False),
101
101
  ]
102
102
 
103
- options = [] # noqa: RUF012
103
+ options = []
104
104
 
105
- annotations = [ # noqa: RUF012
105
+ annotations = [
106
106
  ("state", "TAP state"),
107
107
  ("ir", "Instruction register"),
108
108
  ("dr", "Data register"),
@@ -147,17 +147,7 @@ class JTAGDecoder(SyncDecoder):
147
147
  if tck is None or tms is None or tdi is None:
148
148
  return
149
149
 
150
- n_samples = min(len(tck), len(tms), len(tdi))
151
- if tdo is not None:
152
- n_samples = min(n_samples, len(tdo))
153
-
154
- tck = tck[:n_samples]
155
- tms = tms[:n_samples]
156
- tdi = tdi[:n_samples]
157
- if tdo is not None:
158
- tdo = tdo[:n_samples]
159
-
160
- # Find rising edges of TCK (state updates on rising edge)
150
+ tck, tms, tdi, tdo = self._align_signals(tck, tms, tdi, tdo)
161
151
  rising_edges = np.where(~tck[:-1] & tck[1:])[0] + 1
162
152
 
163
153
  if len(rising_edges) == 0:
@@ -167,95 +157,17 @@ class JTAGDecoder(SyncDecoder):
167
157
  trans_num = 0
168
158
 
169
159
  for edge_idx in rising_edges:
170
- # Sample TMS at rising edge
171
160
  tms_val = bool(tms[edge_idx])
172
-
173
- # Update TAP state
174
161
  new_state = self._next_state(self._tap_state, tms_val)
175
162
 
176
- # Handle state-specific actions
163
+ # Handle data shifting in SHIFT states
177
164
  if self._tap_state in (TAPState.SHIFT_IR, TAPState.SHIFT_DR):
178
- # Shift data
179
- tdi_bit = 1 if tdi[edge_idx] else 0
180
- self._shift_bits_tdi.append(tdi_bit)
165
+ self._shift_data_bit(edge_idx, tdi, tdo)
181
166
 
182
- if tdo is not None:
183
- tdo_bit = 1 if tdo[edge_idx] else 0
184
- self._shift_bits_tdo.append(tdo_bit)
185
-
186
- # Detect state transitions
167
+ # Handle state transitions and emit packets
187
168
  if new_state != self._tap_state:
188
- # Emit packet on state change if we have shifted data
189
- if self._tap_state == TAPState.SHIFT_IR and len(self._shift_bits_tdi) > 0:
190
- # Emit IR shift
191
- ir_value = self._bits_to_value(self._shift_bits_tdi)
192
- start_time = state_start_idx / sample_rate
193
- end_time = edge_idx / sample_rate
194
-
195
- instruction_name = JTAG_INSTRUCTIONS.get(ir_value, "UNKNOWN")
196
-
197
- self.put_annotation(
198
- start_time,
199
- end_time,
200
- AnnotationLevel.FIELDS,
201
- f"IR: 0x{ir_value:02X} ({instruction_name})",
202
- )
203
-
204
- annotations = {
205
- "transaction_num": trans_num,
206
- "tap_state": self._tap_state.value,
207
- "ir_value": ir_value,
208
- "ir_bits": len(self._shift_bits_tdi),
209
- "instruction": instruction_name,
210
- }
211
-
212
- packet = ProtocolPacket(
213
- timestamp=start_time,
214
- protocol="jtag",
215
- data=bytes([ir_value]),
216
- annotations=annotations,
217
- errors=[],
218
- )
219
-
220
- yield packet
221
- trans_num += 1
222
-
223
- elif self._tap_state == TAPState.SHIFT_DR and len(self._shift_bits_tdi) > 0:
224
- # Emit DR shift
225
- dr_value_tdi = self._bits_to_value(self._shift_bits_tdi)
226
- start_time = state_start_idx / sample_rate
227
- end_time = edge_idx / sample_rate
228
-
229
- # Convert to bytes
230
- byte_count = (len(self._shift_bits_tdi) + 7) // 8
231
- dr_bytes = dr_value_tdi.to_bytes(byte_count, "little")
232
-
233
- self.put_annotation(
234
- start_time,
235
- end_time,
236
- AnnotationLevel.FIELDS,
237
- f"DR: 0x{dr_value_tdi:X} ({len(self._shift_bits_tdi)} bits)",
238
- )
239
-
240
- annotations = {
241
- "transaction_num": trans_num,
242
- "tap_state": self._tap_state.value,
243
- "dr_value_tdi": dr_value_tdi,
244
- "dr_bits": len(self._shift_bits_tdi),
245
- }
246
-
247
- if tdo is not None and len(self._shift_bits_tdo) > 0:
248
- dr_value_tdo = self._bits_to_value(self._shift_bits_tdo)
249
- annotations["dr_value_tdo"] = dr_value_tdo
250
-
251
- packet = ProtocolPacket(
252
- timestamp=start_time,
253
- protocol="jtag",
254
- data=dr_bytes,
255
- annotations=annotations,
256
- errors=[],
257
- )
258
-
169
+ packet = self._emit_state_packet(state_start_idx, edge_idx, sample_rate, trans_num)
170
+ if packet is not None:
259
171
  yield packet
260
172
  trans_num += 1
261
173
 
@@ -267,6 +179,176 @@ class JTAGDecoder(SyncDecoder):
267
179
  self._tap_state = new_state
268
180
  state_start_idx = edge_idx
269
181
 
182
+ def _align_signals(
183
+ self,
184
+ tck: NDArray[np.bool_],
185
+ tms: NDArray[np.bool_],
186
+ tdi: NDArray[np.bool_],
187
+ tdo: NDArray[np.bool_] | None,
188
+ ) -> tuple[NDArray[np.bool_], NDArray[np.bool_], NDArray[np.bool_], NDArray[np.bool_] | None]:
189
+ """Align all signals to the same length.
190
+
191
+ Args:
192
+ tck: Test Clock signal.
193
+ tms: Test Mode Select signal.
194
+ tdi: Test Data In signal.
195
+ tdo: Test Data Out signal (optional).
196
+
197
+ Returns:
198
+ Tuple of aligned signals (tck, tms, tdi, tdo).
199
+ """
200
+ n_samples = min(len(tck), len(tms), len(tdi))
201
+ if tdo is not None:
202
+ n_samples = min(n_samples, len(tdo))
203
+
204
+ return (
205
+ tck[:n_samples],
206
+ tms[:n_samples],
207
+ tdi[:n_samples],
208
+ tdo[:n_samples] if tdo is not None else None,
209
+ )
210
+
211
+ def _shift_data_bit(
212
+ self,
213
+ edge_idx: int,
214
+ tdi: NDArray[np.bool_],
215
+ tdo: NDArray[np.bool_] | None,
216
+ ) -> None:
217
+ """Shift a single bit of data on TDI/TDO.
218
+
219
+ Args:
220
+ edge_idx: Edge index where shift occurs.
221
+ tdi: Test Data In signal.
222
+ tdo: Test Data Out signal (optional).
223
+ """
224
+ tdi_bit = 1 if tdi[edge_idx] else 0
225
+ self._shift_bits_tdi.append(tdi_bit)
226
+
227
+ if tdo is not None:
228
+ tdo_bit = 1 if tdo[edge_idx] else 0
229
+ self._shift_bits_tdo.append(tdo_bit)
230
+
231
+ def _emit_state_packet(
232
+ self,
233
+ state_start_idx: int,
234
+ edge_idx: int,
235
+ sample_rate: float,
236
+ trans_num: int,
237
+ ) -> ProtocolPacket | None:
238
+ """Emit packet on state change if we have shifted data.
239
+
240
+ Args:
241
+ state_start_idx: Start index of current state.
242
+ edge_idx: Current edge index.
243
+ sample_rate: Sample rate in Hz.
244
+ trans_num: Transaction number.
245
+
246
+ Returns:
247
+ ProtocolPacket if data was shifted, None otherwise.
248
+ """
249
+ if self._tap_state == TAPState.SHIFT_IR and len(self._shift_bits_tdi) > 0:
250
+ return self._create_ir_packet(state_start_idx, edge_idx, sample_rate, trans_num)
251
+ elif self._tap_state == TAPState.SHIFT_DR and len(self._shift_bits_tdi) > 0:
252
+ return self._create_dr_packet(state_start_idx, edge_idx, sample_rate, trans_num)
253
+ return None
254
+
255
+ def _create_ir_packet(
256
+ self,
257
+ state_start_idx: int,
258
+ edge_idx: int,
259
+ sample_rate: float,
260
+ trans_num: int,
261
+ ) -> ProtocolPacket:
262
+ """Create IR shift packet.
263
+
264
+ Args:
265
+ state_start_idx: Start index of shift state.
266
+ edge_idx: End index of shift state.
267
+ sample_rate: Sample rate in Hz.
268
+ trans_num: Transaction number.
269
+
270
+ Returns:
271
+ ProtocolPacket for IR shift.
272
+ """
273
+ ir_value = self._bits_to_value(self._shift_bits_tdi)
274
+ start_time = state_start_idx / sample_rate
275
+ end_time = edge_idx / sample_rate
276
+ instruction_name = JTAG_INSTRUCTIONS.get(ir_value, "UNKNOWN")
277
+
278
+ self.put_annotation(
279
+ start_time,
280
+ end_time,
281
+ AnnotationLevel.FIELDS,
282
+ f"IR: 0x{ir_value:02X} ({instruction_name})",
283
+ )
284
+
285
+ annotations = {
286
+ "transaction_num": trans_num,
287
+ "tap_state": self._tap_state.value,
288
+ "ir_value": ir_value,
289
+ "ir_bits": len(self._shift_bits_tdi),
290
+ "instruction": instruction_name,
291
+ }
292
+
293
+ return ProtocolPacket(
294
+ timestamp=start_time,
295
+ protocol="jtag",
296
+ data=bytes([ir_value]),
297
+ annotations=annotations,
298
+ errors=[],
299
+ )
300
+
301
+ def _create_dr_packet(
302
+ self,
303
+ state_start_idx: int,
304
+ edge_idx: int,
305
+ sample_rate: float,
306
+ trans_num: int,
307
+ ) -> ProtocolPacket:
308
+ """Create DR shift packet.
309
+
310
+ Args:
311
+ state_start_idx: Start index of shift state.
312
+ edge_idx: End index of shift state.
313
+ sample_rate: Sample rate in Hz.
314
+ trans_num: Transaction number.
315
+
316
+ Returns:
317
+ ProtocolPacket for DR shift.
318
+ """
319
+ dr_value_tdi = self._bits_to_value(self._shift_bits_tdi)
320
+ start_time = state_start_idx / sample_rate
321
+ end_time = edge_idx / sample_rate
322
+
323
+ byte_count = (len(self._shift_bits_tdi) + 7) // 8
324
+ dr_bytes = dr_value_tdi.to_bytes(byte_count, "little")
325
+
326
+ self.put_annotation(
327
+ start_time,
328
+ end_time,
329
+ AnnotationLevel.FIELDS,
330
+ f"DR: 0x{dr_value_tdi:X} ({len(self._shift_bits_tdi)} bits)",
331
+ )
332
+
333
+ annotations = {
334
+ "transaction_num": trans_num,
335
+ "tap_state": self._tap_state.value,
336
+ "dr_value_tdi": dr_value_tdi,
337
+ "dr_bits": len(self._shift_bits_tdi),
338
+ }
339
+
340
+ if len(self._shift_bits_tdo) > 0:
341
+ dr_value_tdo = self._bits_to_value(self._shift_bits_tdo)
342
+ annotations["dr_value_tdo"] = dr_value_tdo
343
+
344
+ return ProtocolPacket(
345
+ timestamp=start_time,
346
+ protocol="jtag",
347
+ data=dr_bytes,
348
+ annotations=annotations,
349
+ errors=[],
350
+ )
351
+
270
352
  def _next_state(self, current: TAPState, tms: bool) -> TAPState:
271
353
  """Compute next TAP state based on TMS value.
272
354
 
@@ -63,13 +63,13 @@ class LINDecoder(AsyncDecoder):
63
63
  longname = "Local Interconnect Network"
64
64
  desc = "LIN automotive bus protocol decoder"
65
65
 
66
- channels = [ # noqa: RUF012
66
+ channels = [
67
67
  ChannelDef("bus", "BUS", "LIN bus signal", required=True),
68
68
  ]
69
69
 
70
- optional_channels = [] # noqa: RUF012
70
+ optional_channels = []
71
71
 
72
- options = [ # noqa: RUF012
72
+ options = [
73
73
  OptionDef(
74
74
  "baudrate",
75
75
  "Baud rate",
@@ -86,7 +86,7 @@ class LINDecoder(AsyncDecoder):
86
86
  ),
87
87
  ]
88
88
 
89
- annotations = [ # noqa: RUF012
89
+ annotations = [
90
90
  ("sync", "Sync field"),
91
91
  ("pid", "Protected identifier"),
92
92
  ("data", "Data bytes"),
@@ -127,14 +127,7 @@ class LINDecoder(AsyncDecoder):
127
127
  >>> for packet in decoder.decode(trace):
128
128
  ... print(f"Data: {packet.data.hex()}")
129
129
  """
130
- # Convert to digital if needed
131
- if isinstance(trace, WaveformTrace):
132
- from oscura.analyzers.digital.extraction import to_digital
133
-
134
- digital_trace = to_digital(trace, threshold="auto")
135
- else:
136
- digital_trace = trace
137
-
130
+ digital_trace = self._prepare_digital_trace(trace)
138
131
  data = digital_trace.data
139
132
  sample_rate = digital_trace.metadata.sample_rate
140
133
 
@@ -145,121 +138,233 @@ class LINDecoder(AsyncDecoder):
145
138
  frame_num = 0
146
139
 
147
140
  while idx < len(data):
148
- # Look for break field (dominant for at least 13 bit times)
149
- break_start = self._find_break_field(data, idx, bit_period)
150
- if break_start is None:
141
+ frame_result = self._try_decode_frame(
142
+ data, idx, bit_period, half_bit, sample_rate, frame_num
143
+ )
144
+
145
+ if frame_result is None:
151
146
  break
152
147
 
153
- # After break field, find the end of the dominant period (break)
154
- # and skip the delimiter (recessive) to reach the sync byte
155
- sync_start_idx = break_start
156
- while sync_start_idx < len(data) and not data[sync_start_idx]:
157
- sync_start_idx += 1
148
+ packet, next_idx = frame_result
149
+ yield packet
158
150
 
159
- # Skip delimiter (recessive period) to reach sync byte start bit
160
- # The delimiter is at least 1 bit time recessive
161
- while sync_start_idx < len(data) and data[sync_start_idx]:
162
- sync_start_idx += 1
151
+ frame_num += 1
152
+ idx = next_idx
163
153
 
164
- if sync_start_idx >= len(data):
165
- break
154
+ def _prepare_digital_trace(self, trace: DigitalTrace | WaveformTrace) -> DigitalTrace:
155
+ """Convert trace to digital format if needed."""
156
+ if isinstance(trace, WaveformTrace):
157
+ from oscura.analyzers.digital.extraction import to_digital
158
+
159
+ return to_digital(trace, threshold="auto")
160
+ return trace
161
+
162
+ def _try_decode_frame(
163
+ self,
164
+ data: NDArray[np.bool_],
165
+ idx: int,
166
+ bit_period: float,
167
+ half_bit: float,
168
+ sample_rate: float,
169
+ frame_num: int,
170
+ ) -> tuple[ProtocolPacket, int] | None:
171
+ """Attempt to decode a single LIN frame."""
172
+ break_start = self._find_break_field(data, idx, bit_period)
173
+ if break_start is None:
174
+ return None
175
+
176
+ sync_start_idx = self._find_sync_start(data, break_start)
177
+ if sync_start_idx >= len(data):
178
+ return None
179
+
180
+ sync_byte, sync_errors = self._decode_sync_field(data, sync_start_idx, bit_period, half_bit)
181
+ pid_byte, pid_errors, frame_id = self._decode_pid_field(
182
+ data, sync_start_idx, bit_period, half_bit
183
+ )
184
+
185
+ if pid_byte is None:
186
+ return None
187
+
188
+ data_length = self._get_data_length(frame_id)
189
+ data_bytes, data_errors = self._decode_data_fields(
190
+ data, sync_start_idx, bit_period, half_bit, data_length
191
+ )
192
+ checksum_byte, checksum_errors = self._decode_checksum_field(
193
+ data, sync_start_idx, bit_period, half_bit, data_length, frame_id, data_bytes
194
+ )
195
+
196
+ packet = self._create_lin_packet(
197
+ break_start,
198
+ sync_start_idx,
199
+ bit_period,
200
+ data_length,
201
+ sample_rate,
202
+ sync_errors,
203
+ pid_errors,
204
+ data_errors,
205
+ checksum_errors,
206
+ frame_num,
207
+ frame_id,
208
+ pid_byte,
209
+ checksum_byte,
210
+ data_bytes,
211
+ )
212
+
213
+ next_idx = int(sync_start_idx + (10 + 10 + data_length * 10 + 10) * bit_period)
214
+ return packet, next_idx
215
+
216
+ def _find_sync_start(self, data: NDArray[np.bool_], break_start: int) -> int:
217
+ """Find start of sync byte after break field."""
218
+ sync_start_idx = break_start
219
+ while sync_start_idx < len(data) and not data[sync_start_idx]:
220
+ sync_start_idx += 1
221
+
222
+ while sync_start_idx < len(data) and data[sync_start_idx]:
223
+ sync_start_idx += 1
224
+
225
+ return sync_start_idx
226
+
227
+ def _decode_sync_field(
228
+ self,
229
+ data: NDArray[np.bool_],
230
+ sync_start_idx: int,
231
+ bit_period: float,
232
+ half_bit: float,
233
+ ) -> tuple[int, list[str]]:
234
+ """Decode and validate sync field."""
235
+ sync_byte, sync_errors = self._decode_byte(data, sync_start_idx, bit_period, half_bit)
236
+
237
+ if sync_byte != 0x55:
238
+ sync_errors.append(f"Invalid sync field: 0x{sync_byte:02X} (expected 0x55)")
239
+
240
+ return sync_byte, sync_errors
166
241
 
167
- # Sync field is 0x55 (01010101)
168
- sync_byte, sync_errors = self._decode_byte(data, sync_start_idx, bit_period, half_bit)
242
+ def _decode_pid_field(
243
+ self,
244
+ data: NDArray[np.bool_],
245
+ sync_start_idx: int,
246
+ bit_period: float,
247
+ half_bit: float,
248
+ ) -> tuple[int | None, list[str], int]:
249
+ """Decode and validate PID field."""
250
+ pid_start_idx = int(sync_start_idx + 10 * bit_period)
251
+ if pid_start_idx >= len(data):
252
+ return None, [], 0
253
+
254
+ pid_byte, pid_errors = self._decode_byte(data, pid_start_idx, bit_period, half_bit)
255
+
256
+ frame_id = pid_byte & 0x3F
257
+ parity = (pid_byte >> 6) & 0x03
258
+ expected_parity = self._compute_parity(frame_id)
259
+
260
+ if parity != expected_parity:
261
+ pid_errors.append(f"Parity error: {parity} (expected {expected_parity})")
169
262
 
170
- if sync_byte != 0x55:
171
- sync_errors.append(f"Invalid sync field: 0x{sync_byte:02X} (expected 0x55)")
263
+ return pid_byte, pid_errors, frame_id
172
264
 
173
- # Protected identifier (PID)
174
- pid_start_idx = int(sync_start_idx + 10 * bit_period)
175
- if pid_start_idx >= len(data):
265
+ def _decode_data_fields(
266
+ self,
267
+ data: NDArray[np.bool_],
268
+ sync_start_idx: int,
269
+ bit_period: float,
270
+ half_bit: float,
271
+ data_length: int,
272
+ ) -> tuple[list[int], list[str]]:
273
+ """Decode all data bytes."""
274
+ data_bytes = []
275
+ data_errors = []
276
+ data_start_idx = int(sync_start_idx + 20 * bit_period)
277
+
278
+ for i in range(data_length):
279
+ byte_start_idx = int(data_start_idx + i * 10 * bit_period)
280
+ if byte_start_idx >= len(data):
176
281
  break
177
282
 
178
- pid_byte, pid_errors = self._decode_byte(data, pid_start_idx, bit_period, half_bit)
179
-
180
- # Extract ID and parity
181
- frame_id = pid_byte & 0x3F
182
- parity = (pid_byte >> 6) & 0x03
183
-
184
- # Validate parity
185
- expected_parity = self._compute_parity(frame_id)
186
- if parity != expected_parity:
187
- pid_errors.append(f"Parity error: {parity} (expected {expected_parity})")
188
-
189
- # Data length depends on frame ID
190
- data_length = self._get_data_length(frame_id)
191
-
192
- # Decode data bytes
193
- data_bytes = []
194
- data_errors = []
195
- data_start_idx = int(pid_start_idx + 10 * bit_period)
196
-
197
- for i in range(data_length):
198
- byte_start_idx = int(data_start_idx + i * 10 * bit_period)
199
- if byte_start_idx >= len(data):
200
- break
201
-
202
- byte_val, byte_errors = self._decode_byte(
203
- data, byte_start_idx, bit_period, half_bit
204
- )
205
- data_bytes.append(byte_val)
206
- data_errors.extend(byte_errors)
207
-
208
- # Decode checksum
209
- checksum_start_idx = int(data_start_idx + data_length * 10 * bit_period)
210
- if checksum_start_idx < len(data):
211
- checksum_byte, checksum_errors = self._decode_byte(
212
- data, checksum_start_idx, bit_period, half_bit
213
- )
214
-
215
- # Validate checksum
216
- expected_checksum = self._compute_checksum(frame_id, data_bytes)
217
- if checksum_byte != expected_checksum:
218
- checksum_errors.append(
219
- f"Checksum error: 0x{checksum_byte:02X} (expected 0x{expected_checksum:02X})"
220
- )
221
- else:
222
- checksum_byte = 0
223
- checksum_errors = ["Missing checksum"]
224
-
225
- # Calculate timestamps
226
- start_time = break_start / sample_rate
227
- end_time = (checksum_start_idx + 10 * bit_period) / sample_rate
228
-
229
- # Collect all errors
230
- errors = sync_errors + pid_errors + data_errors + checksum_errors
231
-
232
- # Add annotations
233
- self.put_annotation(
234
- start_time,
235
- end_time,
236
- AnnotationLevel.PACKETS,
237
- f"ID: 0x{frame_id:02X}",
238
- data=bytes(data_bytes),
239
- )
283
+ byte_val, byte_errors = self._decode_byte(data, byte_start_idx, bit_period, half_bit)
284
+ data_bytes.append(byte_val)
285
+ data_errors.extend(byte_errors)
286
+
287
+ return data_bytes, data_errors
240
288
 
241
- # Create packet
242
- annotations = {
243
- "frame_num": frame_num,
244
- "frame_id": frame_id,
245
- "pid": pid_byte,
246
- "data_length": data_length,
247
- "checksum": checksum_byte,
248
- "version": self._version.value,
249
- }
250
-
251
- packet = ProtocolPacket(
252
- timestamp=start_time,
253
- protocol="lin",
254
- data=bytes(data_bytes),
255
- annotations=annotations,
256
- errors=errors,
289
+ def _decode_checksum_field(
290
+ self,
291
+ data: NDArray[np.bool_],
292
+ sync_start_idx: int,
293
+ bit_period: float,
294
+ half_bit: float,
295
+ data_length: int,
296
+ frame_id: int,
297
+ data_bytes: list[int],
298
+ ) -> tuple[int, list[str]]:
299
+ """Decode and validate checksum field."""
300
+ data_start_idx = int(sync_start_idx + 20 * bit_period)
301
+ checksum_start_idx = int(data_start_idx + data_length * 10 * bit_period)
302
+
303
+ if checksum_start_idx >= len(data):
304
+ return 0, ["Missing checksum"]
305
+
306
+ checksum_byte, checksum_errors = self._decode_byte(
307
+ data, checksum_start_idx, bit_period, half_bit
308
+ )
309
+
310
+ expected_checksum = self._compute_checksum(frame_id, data_bytes)
311
+ if checksum_byte != expected_checksum:
312
+ checksum_errors.append(
313
+ f"Checksum error: 0x{checksum_byte:02X} (expected 0x{expected_checksum:02X})"
257
314
  )
258
315
 
259
- yield packet
316
+ return checksum_byte, checksum_errors
260
317
 
261
- frame_num += 1
262
- idx = int(checksum_start_idx + 10 * bit_period)
318
+ def _create_lin_packet(
319
+ self,
320
+ break_start: int,
321
+ sync_start_idx: int,
322
+ bit_period: float,
323
+ data_length: int,
324
+ sample_rate: float,
325
+ sync_errors: list[str],
326
+ pid_errors: list[str],
327
+ data_errors: list[str],
328
+ checksum_errors: list[str],
329
+ frame_num: int,
330
+ frame_id: int,
331
+ pid_byte: int,
332
+ checksum_byte: int,
333
+ data_bytes: list[int],
334
+ ) -> ProtocolPacket:
335
+ """Create LIN protocol packet from decoded fields."""
336
+ data_start_idx = int(sync_start_idx + 20 * bit_period)
337
+ checksum_start_idx = int(data_start_idx + data_length * 10 * bit_period)
338
+
339
+ start_time = break_start / sample_rate
340
+ end_time = (checksum_start_idx + 10 * bit_period) / sample_rate
341
+
342
+ errors = sync_errors + pid_errors + data_errors + checksum_errors
343
+
344
+ self.put_annotation(
345
+ start_time,
346
+ end_time,
347
+ AnnotationLevel.PACKETS,
348
+ f"ID: 0x{frame_id:02X}",
349
+ data=bytes(data_bytes),
350
+ )
351
+
352
+ annotations = {
353
+ "frame_num": frame_num,
354
+ "frame_id": frame_id,
355
+ "pid": pid_byte,
356
+ "data_length": data_length,
357
+ "checksum": checksum_byte,
358
+ "version": self._version.value,
359
+ }
360
+
361
+ return ProtocolPacket(
362
+ timestamp=start_time,
363
+ protocol="lin",
364
+ data=bytes(data_bytes),
365
+ annotations=annotations,
366
+ errors=errors,
367
+ )
263
368
 
264
369
  def _find_break_field(
265
370
  self,