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
@@ -142,11 +142,11 @@ class CANDecoder(AsyncDecoder):
142
142
  desc = "CAN 2.0A/B bus decoder"
143
143
  license = "MIT"
144
144
 
145
- channels = [ # noqa: RUF012
145
+ channels = [
146
146
  ChannelDef("can", "CAN", "CAN bus signal (CAN_H - CAN_L or single-ended)"),
147
147
  ]
148
148
 
149
- options = [ # noqa: RUF012
149
+ options = [
150
150
  OptionDef(
151
151
  "bitrate",
152
152
  "Bit Rate",
@@ -273,7 +273,7 @@ class CANDecoder(AsyncDecoder):
273
273
  data: NDArray[np.bool_],
274
274
  samples_per_bit: int,
275
275
  ) -> list[int]:
276
- """Find potential frame start positions.
276
+ """Find potential frame start positions using vectorized edge detection.
277
277
 
278
278
  CAN frames start with a Start of Frame (SOF) bit, which is a
279
279
  dominant (0) bit following bus idle (recessive/1).
@@ -285,26 +285,37 @@ class CANDecoder(AsyncDecoder):
285
285
  Returns:
286
286
  List of sample indices for potential frame starts.
287
287
  """
288
- frame_starts = []
289
-
290
- # Look for falling edges (1 -> 0) after idle period
288
+ # Optimize using vectorized operations instead of loop
291
289
  min_idle_bits = 3 # Minimum idle time before frame
292
290
  min_idle_samples = min_idle_bits * samples_per_bit
293
291
 
294
- i = min_idle_samples
295
- while i < len(data) - samples_per_bit:
296
- # Check if previous samples are mostly high (idle)
297
- idle_region = data[max(0, i - min_idle_samples) : i]
292
+ # Detect all falling edges (1 -> 0) using vectorized comparison
293
+ falling_edges = np.where(data[:-1] & ~data[1:])[0] + 1
294
+
295
+ frame_starts = []
296
+
297
+ # Check each falling edge for idle condition
298
+ for edge_idx in falling_edges:
299
+ if edge_idx < min_idle_samples or edge_idx >= len(data) - samples_per_bit:
300
+ continue
301
+
302
+ # Check if previous samples are mostly high (idle) using vectorized mean
303
+ idle_region = data[edge_idx - min_idle_samples : edge_idx]
298
304
  if np.mean(idle_region) > 0.8: # Mostly recessive
299
- # Check for falling edge (SOF)
300
- if data[i - 1] and not data[i]:
301
- frame_starts.append(i)
302
- # Skip ahead to avoid detecting same frame
303
- i += samples_per_bit * 20 # Skip at least 20 bits
304
- continue
305
- i += 1
305
+ frame_starts.append(int(edge_idx))
306
+
307
+ # Filter out closely spaced detections (same frame)
308
+ if not frame_starts:
309
+ return []
310
+
311
+ filtered_starts = [frame_starts[0]]
312
+ min_frame_gap = samples_per_bit * 20 # Minimum gap between frames
306
313
 
307
- return frame_starts
314
+ for start in frame_starts[1:]:
315
+ if start - filtered_starts[-1] >= min_frame_gap:
316
+ filtered_starts.append(start)
317
+
318
+ return filtered_starts
308
319
 
309
320
  def _decode_frame(
310
321
  self,
@@ -407,127 +418,34 @@ class CANDecoder(AsyncDecoder):
407
418
  Returns:
408
419
  Parsed CANFrame or None if invalid.
409
420
  """
410
- errors = []
421
+ errors: list[str] = []
411
422
 
412
423
  try:
413
- pos = 0
414
-
415
- # SOF (should be 0)
416
- if pos >= len(bits):
424
+ pos = self._parse_sof(bits, errors)
425
+ if pos is None:
417
426
  return None
418
- sof = bits[pos]
419
- pos += 1
420
-
421
- if sof != 0:
422
- errors.append("Invalid SOF")
423
427
 
424
- # Arbitration field
425
- if pos + 11 > len(bits):
428
+ arb_result = self._parse_arbitration_field(bits, pos)
429
+ if arb_result[0] is None:
426
430
  return None
431
+ arb_id, is_extended, is_remote, pos = arb_result
427
432
 
428
- # First 11 bits of ID
429
- arb_id = 0
430
- for i in range(11):
431
- arb_id = (arb_id << 1) | bits[pos + i]
432
- pos += 11
433
-
434
- # RTR bit (for standard) or SRR bit (for extended)
435
- if pos >= len(bits):
433
+ dlc_result = self._parse_dlc(bits, pos)
434
+ if dlc_result[0] is None:
436
435
  return None
437
- rtr_or_srr = bits[pos]
438
- pos += 1
436
+ dlc, data_len, pos = dlc_result
439
437
 
440
- # IDE bit
441
- if pos >= len(bits):
442
- return None
443
- ide = bits[pos]
444
- pos += 1
445
-
446
- is_extended = bool(ide)
447
- is_remote = False
448
-
449
- if is_extended:
450
- # Extended frame: 18 more ID bits
451
- if pos + 18 > len(bits):
452
- return None
453
-
454
- # ID extension (18 bits)
455
- for i in range(18):
456
- arb_id = (arb_id << 1) | bits[pos + i]
457
- pos += 18
458
-
459
- # RTR bit
460
- if pos >= len(bits):
461
- return None
462
- is_remote = bool(bits[pos])
463
- pos += 1
464
-
465
- # r1, r0 reserved bits
466
- pos += 2
467
- else:
468
- # Standard frame
469
- is_remote = bool(rtr_or_srr)
470
- # r0 reserved bit
471
- pos += 1
472
-
473
- # DLC (4 bits)
474
- if pos + 4 > len(bits):
438
+ data_result = self._parse_data_field(bits, pos, data_len, is_remote)
439
+ if data_result[0] is None:
475
440
  return None
441
+ data, pos = data_result
476
442
 
477
- dlc = 0
478
- for i in range(4):
479
- dlc = (dlc << 1) | bits[pos + i]
480
- pos += 4
481
-
482
- # Limit DLC to 8
483
- data_len = min(dlc, 8)
484
-
485
- # Data field (0-8 bytes)
486
- if not is_remote:
487
- if pos + data_len * 8 > len(bits):
488
- return None
489
-
490
- data_bytes = bytearray()
491
- for byte_idx in range(data_len):
492
- byte_val = 0
493
- for bit_idx in range(8):
494
- byte_val = (byte_val << 1) | bits[pos + byte_idx * 8 + bit_idx + bit_idx]
495
- data_bytes.append(byte_val)
496
- pos += 8
497
-
498
- data = bytes(data_bytes)
499
- else:
500
- data = b""
501
-
502
- # CRC field (15 bits)
503
- if pos + 15 > len(bits):
443
+ crc_result = self._parse_crc_field(bits, pos, errors)
444
+ if crc_result[0] is None:
504
445
  return None
446
+ crc_received, crc_computed, pos = crc_result
505
447
 
506
- crc_received = 0
507
- for i in range(15):
508
- crc_received = (crc_received << 1) | bits[pos + i]
509
- pos += 15
510
-
511
- # Compute CRC on frame bits before CRC field
512
- # CRC covers SOF through data field
513
- crc_data_end = pos - 15
514
- crc_computed = self._compute_crc(bits[:crc_data_end])
515
-
516
- if crc_received != crc_computed:
517
- errors.append(
518
- f"CRC error: received 0x{crc_received:04X}, computed 0x{crc_computed:04X}"
519
- )
520
-
521
- # CRC delimiter (should be 1)
522
- if pos < len(bits) and bits[pos] != 1:
523
- errors.append("CRC delimiter error")
524
- pos += 1
525
-
526
- # ACK slot and delimiter
527
- pos += 2
528
-
529
- # EOF (7 recessive bits)
530
- # We don't strictly check this
448
+ pos = self._parse_ack_eof(bits, pos, errors)
531
449
 
532
450
  end_time = start_time + pos * (1.0 / self._bitrate)
533
451
 
@@ -547,6 +465,218 @@ class CANDecoder(AsyncDecoder):
547
465
  except (IndexError, ValueError):
548
466
  return None
549
467
 
468
+ def _parse_sof(self, bits: list[int], errors: list[str]) -> int | None:
469
+ """Parse Start of Frame bit.
470
+
471
+ Args:
472
+ bits: Bit array.
473
+ errors: Error list to append to.
474
+
475
+ Returns:
476
+ Position after SOF, or None if invalid.
477
+ """
478
+ if len(bits) < 1:
479
+ return None
480
+
481
+ sof = bits[0]
482
+ if sof != 0:
483
+ errors.append("Invalid SOF")
484
+
485
+ return 1
486
+
487
+ def _parse_arbitration_field(
488
+ self, bits: list[int], pos: int
489
+ ) -> tuple[int, bool, bool, int] | tuple[None, None, None, None]:
490
+ """Parse CAN arbitration field (ID, RTR, IDE).
491
+
492
+ Args:
493
+ bits: Bit array.
494
+ pos: Current position.
495
+
496
+ Returns:
497
+ Tuple of (arb_id, is_extended, is_remote, new_pos) or (None, None, None, None).
498
+ """
499
+ if pos + 11 > len(bits):
500
+ return None, None, None, None
501
+
502
+ # First 11 bits of ID
503
+ arb_id = self._extract_bits_as_int(bits, pos, 11)
504
+ pos += 11
505
+
506
+ # RTR/SRR bit
507
+ if pos >= len(bits):
508
+ return None, None, None, None
509
+ rtr_or_srr = bits[pos]
510
+ pos += 1
511
+
512
+ # IDE bit
513
+ if pos >= len(bits):
514
+ return None, None, None, None
515
+ ide = bits[pos]
516
+ pos += 1
517
+
518
+ is_extended = bool(ide)
519
+
520
+ if is_extended:
521
+ try:
522
+ arb_id, is_remote, pos = self._parse_extended_id(bits, pos, arb_id)
523
+ except IndexError:
524
+ return None, None, None, None
525
+ else:
526
+ is_remote = bool(rtr_or_srr)
527
+ pos += 1 # r0 reserved bit
528
+
529
+ return arb_id, is_extended, is_remote, pos
530
+
531
+ def _parse_extended_id(self, bits: list[int], pos: int, base_id: int) -> tuple[int, bool, int]:
532
+ """Parse extended CAN ID (18 additional bits).
533
+
534
+ Args:
535
+ bits: Bit array.
536
+ pos: Current position.
537
+ base_id: Base 11-bit ID.
538
+
539
+ Returns:
540
+ Tuple of (full_id, is_remote, new_pos).
541
+
542
+ Raises:
543
+ IndexError: If insufficient bits available.
544
+ """
545
+ if pos + 18 > len(bits):
546
+ raise IndexError("Insufficient bits for extended ID")
547
+
548
+ # ID extension (18 bits)
549
+ id_ext = self._extract_bits_as_int(bits, pos, 18)
550
+ arb_id = (base_id << 18) | id_ext
551
+ pos += 18
552
+
553
+ # RTR bit
554
+ if pos >= len(bits):
555
+ raise IndexError("Insufficient bits for RTR")
556
+ is_remote = bool(bits[pos])
557
+ pos += 1
558
+
559
+ # r1, r0 reserved bits
560
+ pos += 2
561
+
562
+ return arb_id, is_remote, pos
563
+
564
+ def _parse_dlc(
565
+ self, bits: list[int], pos: int
566
+ ) -> tuple[int, int, int] | tuple[None, None, None]:
567
+ """Parse Data Length Code.
568
+
569
+ Args:
570
+ bits: Bit array.
571
+ pos: Current position.
572
+
573
+ Returns:
574
+ Tuple of (dlc, data_len, new_pos) or (None, None, None).
575
+ """
576
+ if pos + 4 > len(bits):
577
+ return None, None, None
578
+
579
+ dlc = self._extract_bits_as_int(bits, pos, 4)
580
+ pos += 4
581
+
582
+ data_len = min(dlc, 8) # Limit to 8 bytes
583
+ return dlc, data_len, pos
584
+
585
+ def _parse_data_field(
586
+ self, bits: list[int], pos: int, data_len: int, is_remote: bool
587
+ ) -> tuple[bytes, int] | tuple[None, None]:
588
+ """Parse CAN data field.
589
+
590
+ Args:
591
+ bits: Bit array.
592
+ pos: Current position.
593
+ data_len: Number of data bytes.
594
+ is_remote: True if remote frame.
595
+
596
+ Returns:
597
+ Tuple of (data_bytes, new_pos) or (None, None).
598
+ """
599
+ if is_remote:
600
+ return b"", pos
601
+
602
+ if pos + data_len * 8 > len(bits):
603
+ return None, None
604
+
605
+ data_bytes = bytearray()
606
+ for _ in range(data_len):
607
+ byte_val = self._extract_bits_as_int(bits, pos, 8)
608
+ data_bytes.append(byte_val)
609
+ pos += 8
610
+
611
+ return bytes(data_bytes), pos
612
+
613
+ def _parse_crc_field(
614
+ self, bits: list[int], pos: int, errors: list[str]
615
+ ) -> tuple[int, int, int] | tuple[None, None, None]:
616
+ """Parse CRC field and validate.
617
+
618
+ Args:
619
+ bits: Bit array.
620
+ pos: Current position.
621
+ errors: Error list to append to.
622
+
623
+ Returns:
624
+ Tuple of (crc_received, crc_computed, new_pos) or (None, None, None).
625
+ """
626
+ if pos + 15 > len(bits):
627
+ return None, None, None
628
+
629
+ crc_received = self._extract_bits_as_int(bits, pos, 15)
630
+ crc_data_end = pos
631
+ pos += 15
632
+
633
+ crc_computed = self._compute_crc(bits[:crc_data_end])
634
+
635
+ if crc_received != crc_computed:
636
+ errors.append(
637
+ f"CRC error: received 0x{crc_received:04X}, computed 0x{crc_computed:04X}"
638
+ )
639
+
640
+ # CRC delimiter (should be 1)
641
+ if pos < len(bits) and bits[pos] != 1:
642
+ errors.append("CRC delimiter error")
643
+ pos += 1
644
+
645
+ return crc_received, crc_computed, pos
646
+
647
+ def _parse_ack_eof(self, bits: list[int], pos: int, errors: list[str]) -> int:
648
+ """Parse ACK and EOF fields.
649
+
650
+ Args:
651
+ bits: Bit array.
652
+ pos: Current position.
653
+ errors: Error list to append to.
654
+
655
+ Returns:
656
+ Position after ACK/EOF.
657
+ """
658
+ # ACK slot and delimiter
659
+ pos += 2
660
+
661
+ # EOF (7 recessive bits) - not strictly checked
662
+ return pos
663
+
664
+ def _extract_bits_as_int(self, bits: list[int], start: int, count: int) -> int:
665
+ """Extract consecutive bits as integer (MSB first).
666
+
667
+ Args:
668
+ bits: Bit array.
669
+ start: Start position.
670
+ count: Number of bits.
671
+
672
+ Returns:
673
+ Integer value.
674
+ """
675
+ value = 0
676
+ for i in range(count):
677
+ value = (value << 1) | bits[start + i]
678
+ return value
679
+
550
680
  def _compute_crc(self, bits: list[int]) -> int:
551
681
  """Compute CAN CRC-15.
552
682
 
@@ -120,16 +120,16 @@ class CANFDDecoder(AsyncDecoder):
120
120
  longname = "CAN with Flexible Data-rate"
121
121
  desc = "CAN-FD protocol decoder"
122
122
 
123
- channels = [ # noqa: RUF012
123
+ channels = [
124
124
  ChannelDef("can", "CAN", "CAN bus signal", required=True),
125
125
  ]
126
126
 
127
- optional_channels = [ # noqa: RUF012
127
+ optional_channels = [
128
128
  ChannelDef("can_h", "CAN_H", "CAN High differential signal", required=False),
129
129
  ChannelDef("can_l", "CAN_L", "CAN Low differential signal", required=False),
130
130
  ]
131
131
 
132
- options = [ # noqa: RUF012
132
+ options = [
133
133
  OptionDef(
134
134
  "nominal_bitrate",
135
135
  "Nominal bitrate",
@@ -146,7 +146,7 @@ class CANFDDecoder(AsyncDecoder):
146
146
  ),
147
147
  ]
148
148
 
149
- annotations = [ # noqa: RUF012
149
+ annotations = [
150
150
  ("sof", "Start of Frame"),
151
151
  ("arbitration", "Arbitration field"),
152
152
  ("control", "Control field"),
@@ -299,111 +299,138 @@ class CANFDDecoder(AsyncDecoder):
299
299
  Returns:
300
300
  (frame, end_index) tuple.
301
301
  """
302
- errors = [] # type: ignore[var-annotated]
303
- bit_idx = sof_idx
304
- current_bit_period = nominal_bit_period
305
-
306
- # Sample bits (simplified - ignores bit stuffing for brevity)
307
- def sample_bits(count: int) -> list[int]:
308
- nonlocal bit_idx
309
- bits = []
310
- for _ in range(count):
311
- sample_idx = int(bit_idx + current_bit_period / 2)
312
- if sample_idx < len(data):
313
- bits.append(0 if data[sample_idx] else 1) # Dominant=1, Recessive=0
314
- bit_idx += current_bit_period # type: ignore[assignment]
315
- else:
316
- return bits
317
- return bits
318
-
319
- # Arbitration field (11 bits for standard, 29 for extended)
320
- arb_bits = sample_bits(11)
302
+ decoder_state = _CANFDDecoderState(sof_idx, nominal_bit_period, data, data_bit_period)
303
+
304
+ arbitration_id, is_extended = decoder_state.decode_arbitration_field()
305
+ if arbitration_id is None:
306
+ return None, decoder_state.get_bit_idx()
307
+
308
+ is_fd, brs, esi, dlc = decoder_state.decode_control_field(is_extended)
309
+ if dlc is None:
310
+ return None, decoder_state.get_bit_idx()
311
+
312
+ data_length = CANFD_DLC_TO_LENGTH.get(dlc, 0)
313
+ data_bytes = decoder_state.decode_data_field(data_length, is_fd, brs)
314
+ crc = decoder_state.decode_crc_field(data_length)
315
+ decoder_state.decode_end_of_frame()
316
+
317
+ frame = CANFDFrame(
318
+ arbitration_id=arbitration_id,
319
+ is_extended=is_extended,
320
+ is_fd=is_fd,
321
+ brs=brs,
322
+ esi=esi,
323
+ dlc=dlc,
324
+ data=bytes(data_bytes),
325
+ crc=crc,
326
+ timestamp=sof_idx / sample_rate,
327
+ errors=[],
328
+ )
329
+
330
+ return frame, decoder_state.get_bit_idx()
331
+
332
+
333
+ class _CANFDDecoderState:
334
+ """State tracker for CAN-FD frame decoding."""
335
+
336
+ def __init__(
337
+ self,
338
+ sof_idx: int,
339
+ nominal_bit_period: float,
340
+ data: NDArray[np.bool_],
341
+ data_bit_period: float,
342
+ ):
343
+ self.bit_idx: float = float(sof_idx)
344
+ self.current_bit_period = nominal_bit_period
345
+ self.nominal_bit_period = nominal_bit_period
346
+ self.data_bit_period = data_bit_period
347
+ self.data = data
348
+
349
+ def sample_bits(self, count: int) -> list[int]:
350
+ """Sample specified number of bits."""
351
+ bits = []
352
+ for _ in range(count):
353
+ sample_idx_raw = self.bit_idx + self.current_bit_period / 2
354
+ sample_idx = int(sample_idx_raw)
355
+ if sample_idx < len(self.data):
356
+ bits.append(0 if self.data[sample_idx] else 1)
357
+ self.bit_idx += self.current_bit_period
358
+ else:
359
+ return bits
360
+ return bits
361
+
362
+ def bits_to_int(self, bits: list[int]) -> int:
363
+ """Convert bit list to integer."""
364
+ value = 0
365
+ for bit in bits:
366
+ value = (value << 1) | bit
367
+ return value
368
+
369
+ def decode_arbitration_field(self) -> tuple[int | None, bool]:
370
+ """Decode arbitration field and determine if extended frame."""
371
+ arb_bits = self.sample_bits(11)
321
372
  if len(arb_bits) < 11:
322
- return None, int(bit_idx)
373
+ return None, False
323
374
 
324
- arbitration_id = 0
325
- for bit in arb_bits:
326
- arbitration_id = (arbitration_id << 1) | bit
375
+ arbitration_id = self.bits_to_int(arb_bits)
327
376
 
328
- # Check for extended frame (IDE bit)
329
- ide_bits = sample_bits(1)
330
- is_extended = ide_bits[0] == 1 if ide_bits else False
377
+ ide_bits = self.sample_bits(1)
378
+ is_extended = bool(ide_bits[0]) if ide_bits else False
331
379
 
332
380
  if is_extended:
333
- # Extended ID: read additional 18 bits
334
- ext_bits = sample_bits(18)
335
- for bit in ext_bits:
336
- arbitration_id = (arbitration_id << 1) | bit
381
+ ext_bits = self.sample_bits(18)
382
+ arbitration_id = (arbitration_id << 18) | self.bits_to_int(ext_bits)
383
+
384
+ return arbitration_id, is_extended
337
385
 
338
- # Control field
339
- # FDF (EDL), res, BRS, ESI, DLC (4 bits)
340
- ctrl_bits = sample_bits(7 if not is_extended else 6)
386
+ def decode_control_field(self, is_extended: bool) -> tuple[bool, bool, bool, int | None]:
387
+ """Decode control field."""
388
+ ctrl_bits = self.sample_bits(7 if not is_extended else 6)
341
389
 
342
390
  if len(ctrl_bits) < (7 if not is_extended else 6):
343
- return None, int(bit_idx)
391
+ return False, False, False, None
344
392
 
345
- # FDF/EDL bit - first bit of control field regardless of frame type
346
- fdf = ctrl_bits[0]
347
- is_fd = fdf == 1
393
+ is_fd = ctrl_bits[0] == 1
348
394
  brs = ctrl_bits[2] == 1 if len(ctrl_bits) > 2 else False
349
395
  esi = ctrl_bits[3] == 1 if len(ctrl_bits) > 3 else False
350
396
 
351
- # DLC (4 bits)
352
397
  dlc_start = 3 if not is_extended else 2
353
398
  dlc_bits = (
354
399
  ctrl_bits[dlc_start : dlc_start + 4]
355
400
  if len(ctrl_bits) >= dlc_start + 4
356
401
  else [0, 0, 0, 0]
357
402
  )
358
- dlc = 0
359
- for bit in dlc_bits:
360
- dlc = (dlc << 1) | bit
403
+ dlc = self.bits_to_int(dlc_bits)
361
404
 
362
- # Get data length from DLC
363
- data_length = CANFD_DLC_TO_LENGTH.get(dlc, 0)
405
+ return is_fd, brs, esi, dlc
364
406
 
365
- # Switch to data bitrate if BRS is set
407
+ def decode_data_field(self, data_length: int, is_fd: bool, brs: bool) -> list[int]:
408
+ """Decode data field."""
366
409
  if is_fd and brs:
367
- current_bit_period = data_bit_period
410
+ self.current_bit_period = self.data_bit_period
368
411
 
369
- # Data field
370
412
  data_bytes = []
371
413
  for _ in range(data_length):
372
- byte_bits = sample_bits(8)
414
+ byte_bits = self.sample_bits(8)
373
415
  if len(byte_bits) == 8:
374
- byte_val = 0
375
- for bit in byte_bits:
376
- byte_val = (byte_val << 1) | bit
377
- data_bytes.append(byte_val)
378
-
379
- # CRC field (CRC-17 for <=16 bytes, CRC-21 for >16 bytes)
380
- crc_length = 17 if data_length <= 16 else 21
381
- crc_bits = sample_bits(crc_length)
382
- crc = 0
383
- for bit in crc_bits:
384
- crc = (crc << 1) | bit
416
+ data_bytes.append(self.bits_to_int(byte_bits))
385
417
 
386
- # Switch back to nominal bitrate for CRC delimiter, ACK, EOF
387
- current_bit_period = nominal_bit_period
418
+ return data_bytes
388
419
 
389
- # CRC delimiter, ACK slot, ACK delimiter, EOF (7 bits)
390
- sample_bits(10)
420
+ def decode_crc_field(self, data_length: int) -> int:
421
+ """Decode CRC field."""
422
+ crc_length = 17 if data_length <= 16 else 21
423
+ crc_bits = self.sample_bits(crc_length)
424
+ return self.bits_to_int(crc_bits)
391
425
 
392
- # Create frame
393
- frame = CANFDFrame(
394
- arbitration_id=arbitration_id,
395
- is_extended=is_extended,
396
- is_fd=is_fd,
397
- brs=brs,
398
- esi=esi,
399
- dlc=dlc,
400
- data=bytes(data_bytes),
401
- crc=crc,
402
- timestamp=sof_idx / sample_rate,
403
- errors=errors,
404
- )
426
+ def decode_end_of_frame(self) -> None:
427
+ """Decode end of frame."""
428
+ self.current_bit_period = self.nominal_bit_period
429
+ self.sample_bits(10)
405
430
 
406
- return frame, int(bit_idx)
431
+ def get_bit_idx(self) -> int:
432
+ """Get current bit index."""
433
+ return int(self.bit_idx)
407
434
 
408
435
 
409
436
  def decode_can_fd(