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
@@ -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,