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