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
@@ -9,7 +9,14 @@ from __future__ import annotations
9
9
 
10
10
  from typing import TYPE_CHECKING, Any
11
11
 
12
- import pandas as pd
12
+ # Lazy import for optional dataframe support
13
+ try:
14
+ import pandas as pd
15
+
16
+ _HAS_PANDAS = True
17
+ except ImportError:
18
+ pd = None # type: ignore[assignment]
19
+ _HAS_PANDAS = False
13
20
 
14
21
  from oscura.automotive.can.analysis import MessageAnalyzer
15
22
  from oscura.automotive.can.models import (
@@ -51,7 +58,7 @@ class CANSession(AnalysisSession):
51
58
 
52
59
  Example - Basic usage:
53
60
  >>> from oscura.sessions import CANSession
54
- >>> from oscura.acquisition import FileSource
61
+ >>> from oscura.hardware.acquisition import FileSource
55
62
  >>> session = CANSession(name="Vehicle Analysis")
56
63
  >>> session.add_recording("baseline", FileSource("idle.blf"))
57
64
  >>> inventory = session.inventory()
@@ -80,23 +87,38 @@ class CANSession(AnalysisSession):
80
87
  >>> print(f"Changed messages: {result.changed_bytes}")
81
88
  """
82
89
 
83
- def __init__(self, name: str = "CAN Session"):
90
+ def __init__(
91
+ self,
92
+ name: str = "CAN Session",
93
+ auto_crc: bool = True,
94
+ crc_validate: bool = True,
95
+ crc_min_messages: int = 10,
96
+ ):
84
97
  """Initialize CAN session.
85
98
 
86
99
  Args:
87
100
  name: Session name (default: "CAN Session").
101
+ auto_crc: Enable automatic CRC recovery (default: True).
102
+ crc_validate: Validate CRCs on subsequent messages (default: True).
103
+ crc_min_messages: Minimum messages per ID for CRC recovery (default: 10).
88
104
 
89
105
  Example:
90
106
  >>> from oscura.sessions import CANSession
91
- >>> from oscura.acquisition import FileSource
107
+ >>> from oscura.hardware.acquisition import FileSource
92
108
  >>> session = CANSession(name="Vehicle Analysis")
93
109
  >>> session.add_recording("baseline", FileSource("idle.blf"))
94
110
  >>> session.add_recording("active", FileSource("running.blf"))
95
111
  >>> results = session.analyze()
112
+ >>> # CRC parameters automatically recovered during analysis
113
+ >>> print(session.crc_info)
96
114
  """
97
115
  super().__init__(name=name)
98
116
  self._messages = CANMessageList()
99
117
  self._analyses_cache: dict[int, MessageAnalysis] = {}
118
+ self.auto_crc = auto_crc
119
+ self.crc_validate = crc_validate
120
+ self.crc_min_messages = crc_min_messages
121
+ self._crc_params: dict[int, Any] = {} # Map CAN ID to CRCParameters
100
122
 
101
123
  def inventory(self) -> pd.DataFrame:
102
124
  """Generate message inventory.
@@ -112,7 +134,19 @@ class CANSession(AnalysisSession):
112
134
 
113
135
  Returns:
114
136
  DataFrame with message inventory.
137
+
138
+ Raises:
139
+ ImportError: If pandas is not installed.
115
140
  """
141
+ if not _HAS_PANDAS:
142
+ raise ImportError(
143
+ "Message inventory requires pandas.\n\n"
144
+ "Install with:\n"
145
+ " pip install oscura[dataframes] # DataFrame support\n"
146
+ " pip install oscura[standard] # Recommended\n"
147
+ " pip install oscura[all] # Everything\n"
148
+ )
149
+
116
150
  unique_ids = sorted(self._messages.unique_ids())
117
151
 
118
152
  inventory_data = []
@@ -213,58 +247,139 @@ class CANSession(AnalysisSession):
213
247
  This creates a new session with filtered messages from the current
214
248
  internal message collection. This method is primarily for legacy
215
249
  workflows. For new code, use add_recording() with separate files.
250
+
251
+ Example:
252
+ >>> session.filter(min_frequency=10.0, arbitration_ids=[0x123, 0x456])
216
253
  """
217
- filtered_messages = []
218
-
219
- # First, filter by time range if specified
220
- if time_range:
221
- start_time, end_time = time_range
222
- for msg in self._messages:
223
- if start_time <= msg.timestamp <= end_time:
224
- filtered_messages.append(msg)
225
- else:
226
- filtered_messages = list(self._messages)
227
-
228
- # Filter by CAN IDs if specified
229
- if arbitration_ids:
230
- filtered_messages = [
231
- msg for msg in filtered_messages if msg.arbitration_id in arbitration_ids
232
- ]
233
-
234
- # Filter by frequency if specified
235
- if min_frequency or max_frequency:
236
- # Group by ID and calculate frequencies
237
- from collections import defaultdict
238
-
239
- id_messages: dict[int, list[CANMessage]] = defaultdict(list)
240
- for msg in filtered_messages:
241
- id_messages[msg.arbitration_id].append(msg)
242
-
243
- # Filter IDs by frequency
244
- valid_ids = set()
245
- for arb_id, msgs in id_messages.items():
246
- if len(msgs) > 1:
247
- timestamps = [m.timestamp for m in msgs]
248
- duration = max(timestamps) - min(timestamps)
249
- if duration > 0:
250
- freq = (len(msgs) - 1) / duration
251
-
252
- if min_frequency and freq < min_frequency:
253
- continue
254
- if max_frequency and freq > max_frequency:
255
- continue
256
-
257
- valid_ids.add(arb_id)
258
-
259
- filtered_messages = [
260
- msg for msg in filtered_messages if msg.arbitration_id in valid_ids
261
- ]
254
+ # Apply filters sequentially
255
+ filtered_messages = self._filter_by_time_range(time_range)
256
+ filtered_messages = self._filter_by_arbitration_ids(filtered_messages, arbitration_ids)
257
+ filtered_messages = self._filter_by_frequency(
258
+ filtered_messages, min_frequency, max_frequency
259
+ )
262
260
 
263
261
  # Create new session with filtered messages
264
262
  new_session = CANSession(name=f"{self.name} (filtered)")
265
263
  new_session._messages = CANMessageList(messages=filtered_messages)
266
264
  return new_session
267
265
 
266
+ def _filter_by_time_range(self, time_range: tuple[float, float] | None) -> list[CANMessage]:
267
+ """Filter messages by timestamp range.
268
+
269
+ Args:
270
+ time_range: Optional (start_time, end_time) in seconds.
271
+
272
+ Returns:
273
+ Filtered message list.
274
+ """
275
+ if time_range is None:
276
+ return list(self._messages)
277
+
278
+ start_time, end_time = time_range
279
+ return [msg for msg in self._messages if start_time <= msg.timestamp <= end_time]
280
+
281
+ def _filter_by_arbitration_ids(
282
+ self, messages: list[CANMessage], arbitration_ids: list[int] | None
283
+ ) -> list[CANMessage]:
284
+ """Filter messages by CAN arbitration IDs.
285
+
286
+ Args:
287
+ messages: Messages to filter.
288
+ arbitration_ids: Optional list of CAN IDs to include.
289
+
290
+ Returns:
291
+ Filtered message list.
292
+ """
293
+ if arbitration_ids is None:
294
+ return messages
295
+
296
+ id_set = set(arbitration_ids)
297
+ return [msg for msg in messages if msg.arbitration_id in id_set]
298
+
299
+ def _filter_by_frequency(
300
+ self,
301
+ messages: list[CANMessage],
302
+ min_frequency: float | None,
303
+ max_frequency: float | None,
304
+ ) -> list[CANMessage]:
305
+ """Filter messages by transmission frequency.
306
+
307
+ Args:
308
+ messages: Messages to filter.
309
+ min_frequency: Optional minimum frequency in Hz.
310
+ max_frequency: Optional maximum frequency in Hz.
311
+
312
+ Returns:
313
+ Filtered message list (only IDs with frequency in range).
314
+ """
315
+ if min_frequency is None and max_frequency is None:
316
+ return messages
317
+
318
+ # Calculate frequency for each CAN ID
319
+ valid_ids = self._find_ids_in_frequency_range(messages, min_frequency, max_frequency)
320
+
321
+ return [msg for msg in messages if msg.arbitration_id in valid_ids]
322
+
323
+ def _find_ids_in_frequency_range(
324
+ self,
325
+ messages: list[CANMessage],
326
+ min_frequency: float | None,
327
+ max_frequency: float | None,
328
+ ) -> set[int]:
329
+ """Find CAN IDs with transmission frequency in specified range.
330
+
331
+ Args:
332
+ messages: Messages to analyze.
333
+ min_frequency: Optional minimum frequency in Hz.
334
+ max_frequency: Optional maximum frequency in Hz.
335
+
336
+ Returns:
337
+ Set of CAN IDs meeting frequency criteria.
338
+ """
339
+ from collections import defaultdict
340
+
341
+ # Group messages by ID
342
+ id_messages: dict[int, list[CANMessage]] = defaultdict(list)
343
+ for msg in messages:
344
+ id_messages[msg.arbitration_id].append(msg)
345
+
346
+ # Check frequency for each ID
347
+ valid_ids = set()
348
+ for arb_id, msgs in id_messages.items():
349
+ freq = self._calculate_message_frequency(msgs)
350
+ if freq is None:
351
+ continue
352
+
353
+ # Check frequency bounds
354
+ if min_frequency is not None and freq < min_frequency:
355
+ continue
356
+ if max_frequency is not None and freq > max_frequency:
357
+ continue
358
+
359
+ valid_ids.add(arb_id)
360
+
361
+ return valid_ids
362
+
363
+ def _calculate_message_frequency(self, messages: list[CANMessage]) -> float | None:
364
+ """Calculate transmission frequency for a list of messages.
365
+
366
+ Args:
367
+ messages: Messages with same arbitration ID.
368
+
369
+ Returns:
370
+ Frequency in Hz, or None if cannot be calculated.
371
+ """
372
+ if len(messages) <= 1:
373
+ return None
374
+
375
+ timestamps = [m.timestamp for m in messages]
376
+ duration = max(timestamps) - min(timestamps)
377
+
378
+ if duration <= 0:
379
+ return None
380
+
381
+ return (len(messages) - 1) / duration
382
+
268
383
  def unique_ids(self) -> set[int]:
269
384
  """Get set of unique CAN IDs in this session.
270
385
 
@@ -302,7 +417,7 @@ class CANSession(AnalysisSession):
302
417
 
303
418
  Example:
304
419
  >>> from oscura.sessions import CANSession
305
- >>> from oscura.acquisition import FileSource
420
+ >>> from oscura.hardware.acquisition import FileSource
306
421
  >>> session = CANSession(name="Analysis")
307
422
  >>> session.add_recording("data", FileSource("capture.blf"))
308
423
  >>> results = session.analyze()
@@ -316,6 +431,10 @@ class CANSession(AnalysisSession):
316
431
  # Generate inventory
317
432
  inventory = self.inventory()
318
433
 
434
+ # Automatically recover CRC parameters if enabled
435
+ if self.auto_crc:
436
+ self._auto_recover_crc()
437
+
319
438
  # Analyze all unique IDs
320
439
  message_analyses = {}
321
440
  for arb_id in self.unique_ids():
@@ -368,7 +487,7 @@ class CANSession(AnalysisSession):
368
487
  - details: CAN-specific details (changed_ids, byte_changes, etc.)
369
488
 
370
489
  Example:
371
- >>> from oscura.acquisition import FileSource
490
+ >>> from oscura.hardware.acquisition import FileSource
372
491
  >>> session = CANSession(name="Brake Analysis")
373
492
  >>> session.add_recording("no_brake", FileSource("idle.blf"))
374
493
  >>> session.add_recording("brake_pressed", FileSource("brake.blf"))
@@ -416,7 +535,7 @@ class CANSession(AnalysisSession):
416
535
  recording1=name1,
417
536
  recording2=name2,
418
537
  changed_bytes=total_byte_changes,
419
- changed_regions=changed_regions, # type: ignore[arg-type]
538
+ changed_regions=changed_regions,
420
539
  similarity_score=similarity,
421
540
  details={
422
541
  "changed_message_ids": changed_message_ids,
@@ -520,7 +639,7 @@ class CANSession(AnalysisSession):
520
639
 
521
640
  Example:
522
641
  >>> from oscura.sessions import CANSession
523
- >>> from oscura.acquisition import FileSource
642
+ >>> from oscura.hardware.acquisition import FileSource
524
643
  >>> session = CANSession(name="Pattern Analysis")
525
644
  >>> session.add_recording("data", FileSource("capture.blf"))
526
645
  >>> pairs = session.find_message_pairs(time_window_ms=50)
@@ -554,7 +673,7 @@ class CANSession(AnalysisSession):
554
673
 
555
674
  Example:
556
675
  >>> from oscura.sessions import CANSession
557
- >>> from oscura.acquisition import FileSource
676
+ >>> from oscura.hardware.acquisition import FileSource
558
677
  >>> session = CANSession(name="Sequence Analysis")
559
678
  >>> session.add_recording("data", FileSource("startup.blf"))
560
679
  >>> sequences = session.find_message_sequences(
@@ -590,7 +709,7 @@ class CANSession(AnalysisSession):
590
709
 
591
710
  Example:
592
711
  >>> from oscura.sessions import CANSession
593
- >>> from oscura.acquisition import FileSource
712
+ >>> from oscura.hardware.acquisition import FileSource
594
713
  >>> session = CANSession(name="Correlation Analysis")
595
714
  >>> session.add_recording("data", FileSource("capture.blf"))
596
715
  >>> correlations = session.find_temporal_correlations(max_delay_ms=50)
@@ -618,7 +737,7 @@ class CANSession(AnalysisSession):
618
737
 
619
738
  Example:
620
739
  >>> from oscura.sessions import CANSession
621
- >>> from oscura.acquisition import FileSource
740
+ >>> from oscura.hardware.acquisition import FileSource
622
741
  >>> session = CANSession(name="State Machine Learning")
623
742
  >>> session.add_recording("data", FileSource("ignition_cycles.blf"))
624
743
  >>> automaton = session.learn_state_machine(
@@ -633,6 +752,123 @@ class CANSession(AnalysisSession):
633
752
  session=self, trigger_ids=trigger_ids, context_window_ms=context_window_ms
634
753
  )
635
754
 
755
+ def _auto_recover_crc(self) -> None:
756
+ """Automatically recover CRC parameters for each message ID.
757
+
758
+ This method attempts to recover CRC parameters for each CAN ID that has
759
+ enough messages. Recovered parameters are stored in self._crc_params.
760
+
761
+ Note:
762
+ Only attempts recovery for IDs with >= crc_min_messages.
763
+ Requires confidence > 0.8 to accept recovered parameters.
764
+ """
765
+ import logging
766
+
767
+ from oscura.inference.crc_reverse import CRCReverser
768
+
769
+ logger = logging.getLogger(__name__)
770
+
771
+ for arb_id in self.unique_ids():
772
+ filtered = self._messages.filter_by_id(arb_id)
773
+ if len(filtered) < self.crc_min_messages:
774
+ continue
775
+
776
+ try:
777
+ # Prepare message-CRC pairs
778
+ # Heuristic: try last 1-4 bytes as potential CRC
779
+ messages = []
780
+ for msg in filtered.messages:
781
+ if msg.dlc >= 3:
782
+ # Try 1-byte CRC at end
783
+ messages.append((bytes(msg.data[:-1]), bytes(msg.data[-1:])))
784
+
785
+ if len(messages) >= 4:
786
+ reverser = CRCReverser(verbose=False)
787
+ params = reverser.reverse(messages)
788
+
789
+ if params is not None and params.confidence > 0.8:
790
+ self._crc_params[arb_id] = params
791
+ logger.info(
792
+ f"Auto-recovered CRC for 0x{arb_id:03X}: "
793
+ f"poly=0x{params.polynomial:04x}, "
794
+ f"width={params.width}, confidence={params.confidence:.2f}"
795
+ )
796
+
797
+ except Exception:
798
+ # CRC recovery is best-effort
799
+ pass
800
+
801
+ def _validate_crc(self, msg: CANMessage) -> bool:
802
+ """Validate CRC for a message if parameters are known.
803
+
804
+ Args:
805
+ msg: CAN message to validate.
806
+
807
+ Returns:
808
+ True if CRC is valid or no CRC params available, False if invalid.
809
+
810
+ Note:
811
+ Logs warning if CRC validation fails.
812
+ """
813
+ if not self.crc_validate or msg.arbitration_id not in self._crc_params:
814
+ return True
815
+
816
+ import logging
817
+
818
+ from oscura.inference.crc_reverse import verify_crc
819
+
820
+ logger = logging.getLogger(__name__)
821
+ params = self._crc_params[msg.arbitration_id]
822
+
823
+ try:
824
+ # Extract message data and CRC based on width
825
+ crc_bytes = params.width // 8
826
+ if msg.dlc < crc_bytes:
827
+ return True # Message too short, skip validation
828
+
829
+ data = bytes(msg.data[:-crc_bytes])
830
+ crc = bytes(msg.data[-crc_bytes:])
831
+
832
+ if not verify_crc(data, crc, params):
833
+ logger.warning(
834
+ f"CRC validation failed for 0x{msg.arbitration_id:03X} "
835
+ f"at timestamp {msg.timestamp:.6f}"
836
+ )
837
+ return False
838
+
839
+ except Exception:
840
+ # Validation errors are non-fatal
841
+ pass
842
+
843
+ return True
844
+
845
+ @property
846
+ def crc_info(self) -> dict[int, dict[str, Any]]:
847
+ """Get recovered CRC information for all message IDs.
848
+
849
+ Returns:
850
+ Dictionary mapping CAN ID to CRC parameter dict.
851
+
852
+ Example:
853
+ >>> session = CANSession()
854
+ >>> # ... add recordings and analyze ...
855
+ >>> for can_id, params in session.crc_info.items():
856
+ ... print(f"0x{can_id:03X}: {params['algorithm_name']}")
857
+ """
858
+ result = {}
859
+ for arb_id, params in self._crc_params.items():
860
+ result[arb_id] = {
861
+ "polynomial": f"0x{params.polynomial:04x}",
862
+ "width": params.width,
863
+ "init": f"0x{params.init:04x}",
864
+ "xor_out": f"0x{params.xor_out:04x}",
865
+ "reflect_in": params.reflect_in,
866
+ "reflect_out": params.reflect_out,
867
+ "confidence": params.confidence,
868
+ "algorithm_name": params.algorithm_name,
869
+ }
870
+ return result
871
+
636
872
  def __repr__(self) -> str:
637
873
  """Human-readable representation."""
638
874
  num_messages = len(self._messages)
@@ -20,7 +20,7 @@ Use cases:
20
20
  from __future__ import annotations
21
21
 
22
22
  from dataclasses import dataclass
23
- from typing import TYPE_CHECKING
23
+ from typing import TYPE_CHECKING, cast
24
24
 
25
25
  from oscura.inference.state_machine import FiniteAutomaton, StateMachineInferrer
26
26
 
@@ -152,7 +152,8 @@ class CANStateMachine:
152
152
  )
153
153
 
154
154
  # Learn state machine using RPNI
155
- automaton = self._inferrer.infer_rpni(positive_traces=traces)
155
+ # Cast: list[list[str]] is compatible with list[list[str | int]] at runtime
156
+ automaton = self._inferrer.infer_rpni(positive_traces=cast("list[list[str | int]]", traces))
156
157
 
157
158
  return automaton
158
159
 
@@ -275,7 +276,9 @@ class CANStateMachine:
275
276
  )
276
277
 
277
278
  # Learn state machine
278
- automaton = self._inferrer.infer_rpni(positive_traces=state_sequences)
279
+ # Cast: list[list[str]] is compatible with list[list[str | int]] at runtime
280
+ state_sequences_union: list[list[str | int]] = state_sequences # type: ignore[assignment]
281
+ automaton = self._inferrer.infer_rpni(positive_traces=state_sequences_union)
279
282
 
280
283
  return automaton
281
284
 
@@ -17,9 +17,10 @@ This allows answering questions like:
17
17
  from __future__ import annotations
18
18
 
19
19
  from dataclasses import dataclass, field
20
- from typing import TYPE_CHECKING
20
+ from typing import TYPE_CHECKING, Any
21
21
 
22
22
  import numpy as np
23
+ from numpy.typing import NDArray
23
24
  from scipy import stats
24
25
 
25
26
  if TYPE_CHECKING:
@@ -262,14 +263,12 @@ class StimulusResponseAnalyzer:
262
263
  Returns:
263
264
  List of ByteChange objects for changed bytes.
264
265
  """
265
- # Get messages for this ID from both sessions
266
266
  baseline_msgs = baseline_session._messages.filter_by_id(message_id)
267
267
  stimulus_msgs = stimulus_session._messages.filter_by_id(message_id)
268
268
 
269
269
  if not baseline_msgs.messages or not stimulus_msgs.messages:
270
270
  return []
271
271
 
272
- # Determine max DLC
273
272
  max_dlc = max(
274
273
  max(msg.dlc for msg in baseline_msgs.messages),
275
274
  max(msg.dlc for msg in stimulus_msgs.messages),
@@ -277,82 +276,105 @@ class StimulusResponseAnalyzer:
277
276
 
278
277
  changes = []
279
278
  for byte_pos in range(max_dlc):
280
- # Extract byte values from both sessions
281
- baseline_values = [
282
- msg.data[byte_pos] for msg in baseline_msgs.messages if len(msg.data) > byte_pos
283
- ]
284
- stimulus_values = [
285
- msg.data[byte_pos] for msg in stimulus_msgs.messages if len(msg.data) > byte_pos
286
- ]
287
-
288
- if not baseline_values or not stimulus_values:
289
- continue
290
-
291
- # Analyze changes
292
- baseline_set = set(baseline_values)
293
- stimulus_set = set(stimulus_values)
294
-
295
- # Skip if not enough unique values
296
- if len(baseline_set) < byte_threshold and len(stimulus_set) < byte_threshold:
297
- continue
298
-
299
- # Calculate statistics
300
- baseline_arr = np.array(baseline_values)
301
- stimulus_arr = np.array(stimulus_values)
302
-
303
- baseline_mean = float(np.mean(baseline_arr))
304
- stimulus_mean = float(np.mean(stimulus_arr))
305
- mean_change = stimulus_mean - baseline_mean
306
-
307
- baseline_range = float(np.max(baseline_arr) - np.min(baseline_arr))
308
- stimulus_range = float(np.max(stimulus_arr) - np.min(stimulus_arr))
309
- value_range_change = stimulus_range - baseline_range
310
-
311
- # Calculate normalized change magnitude using multiple factors
312
- # 1. Mean change (normalized by full byte range)
313
- mean_change_norm = abs(mean_change) / 255.0
314
-
315
- # 2. Range change (normalized by full byte range)
316
- range_change_norm = abs(value_range_change) / 255.0
317
-
318
- # 3. Set difference (Jaccard distance)
319
- union_size = len(baseline_set | stimulus_set)
320
- intersection_size = len(baseline_set & stimulus_set)
321
- if union_size > 0:
322
- jaccard_dist = 1.0 - (intersection_size / union_size)
323
- else:
324
- jaccard_dist = 0.0
325
-
326
- # 4. Distribution change (Kolmogorov-Smirnov test)
327
- try:
328
- ks_stat, _ = stats.ks_2samp(baseline_arr, stimulus_arr)
329
- ks_change_norm = float(ks_stat)
330
- except Exception:
331
- ks_change_norm = 0.0
332
-
333
- # Combine factors (weighted average)
334
- change_magnitude = (
335
- 0.3 * mean_change_norm
336
- + 0.2 * range_change_norm
337
- + 0.3 * jaccard_dist
338
- + 0.2 * ks_change_norm
279
+ change = self._analyze_byte_change(
280
+ baseline_msgs, stimulus_msgs, byte_pos, byte_threshold
339
281
  )
340
-
341
- # Only report if there's a meaningful change
342
- if change_magnitude > 0.0:
343
- changes.append(
344
- ByteChange(
345
- byte_position=byte_pos,
346
- baseline_values=baseline_set,
347
- stimulus_values=stimulus_set,
348
- change_magnitude=change_magnitude,
349
- value_range_change=value_range_change,
350
- mean_change=mean_change,
351
- )
352
- )
282
+ if change:
283
+ changes.append(change)
353
284
 
354
285
  return changes
355
286
 
287
+ def _analyze_byte_change(
288
+ self, baseline_msgs: Any, stimulus_msgs: Any, byte_pos: int, byte_threshold: int
289
+ ) -> ByteChange | None:
290
+ """Analyze change for a single byte position."""
291
+ # Extract byte values
292
+ baseline_values = [
293
+ msg.data[byte_pos] for msg in baseline_msgs.messages if len(msg.data) > byte_pos
294
+ ]
295
+ stimulus_values = [
296
+ msg.data[byte_pos] for msg in stimulus_msgs.messages if len(msg.data) > byte_pos
297
+ ]
298
+
299
+ if not baseline_values or not stimulus_values:
300
+ return None
301
+
302
+ baseline_set, stimulus_set = set(baseline_values), set(stimulus_values)
303
+
304
+ # Skip if not enough unique values
305
+ if len(baseline_set) < byte_threshold and len(stimulus_set) < byte_threshold:
306
+ return None
307
+
308
+ # Calculate statistics and change magnitude
309
+ baseline_arr, stimulus_arr = np.array(baseline_values), np.array(stimulus_values)
310
+ mean_change, value_range_change = self._compute_byte_stats(baseline_arr, stimulus_arr)
311
+ change_magnitude = self._compute_change_magnitude(
312
+ baseline_arr, stimulus_arr, baseline_set, stimulus_set, mean_change, value_range_change
313
+ )
314
+
315
+ if change_magnitude <= 0.0:
316
+ return None
317
+
318
+ return ByteChange(
319
+ byte_position=byte_pos,
320
+ baseline_values=baseline_set,
321
+ stimulus_values=stimulus_set,
322
+ change_magnitude=change_magnitude,
323
+ value_range_change=value_range_change,
324
+ mean_change=mean_change,
325
+ )
326
+
327
+ def _compute_byte_stats(
328
+ self, baseline_arr: NDArray[np.int_], stimulus_arr: NDArray[np.int_]
329
+ ) -> tuple[float, float]:
330
+ """Compute mean and range changes for byte values."""
331
+ baseline_mean, stimulus_mean = float(np.mean(baseline_arr)), float(np.mean(stimulus_arr))
332
+ mean_change = stimulus_mean - baseline_mean
333
+
334
+ baseline_range = float(np.max(baseline_arr) - np.min(baseline_arr))
335
+ stimulus_range = float(np.max(stimulus_arr) - np.min(stimulus_arr))
336
+ value_range_change = stimulus_range - baseline_range
337
+
338
+ return mean_change, value_range_change
339
+
340
+ def _compute_change_magnitude(
341
+ self,
342
+ baseline_arr: NDArray[np.int_],
343
+ stimulus_arr: NDArray[np.int_],
344
+ baseline_set: set[int],
345
+ stimulus_set: set[int],
346
+ mean_change: float,
347
+ value_range_change: float,
348
+ ) -> float:
349
+ """Compute normalized change magnitude using multiple factors."""
350
+ # 1. Mean change (normalized by full byte range)
351
+ mean_change_norm = abs(mean_change) / 255.0
352
+
353
+ # 2. Range change (normalized by full byte range)
354
+ range_change_norm = abs(value_range_change) / 255.0
355
+
356
+ # 3. Set difference (Jaccard distance)
357
+ union_size, intersection_size = (
358
+ len(baseline_set | stimulus_set),
359
+ len(baseline_set & stimulus_set),
360
+ )
361
+ jaccard_dist = 1.0 - (intersection_size / union_size) if union_size > 0 else 0.0
362
+
363
+ # 4. Distribution change (Kolmogorov-Smirnov test)
364
+ try:
365
+ ks_stat, _ = stats.ks_2samp(baseline_arr, stimulus_arr)
366
+ ks_change_norm = float(ks_stat)
367
+ except Exception:
368
+ ks_change_norm = 0.0
369
+
370
+ # Combine factors (weighted average)
371
+ return (
372
+ 0.3 * mean_change_norm
373
+ + 0.2 * range_change_norm
374
+ + 0.3 * jaccard_dist
375
+ + 0.2 * ks_change_norm
376
+ )
377
+
356
378
  def find_responsive_messages(
357
379
  self,
358
380
  baseline_session: CANSession,