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
@@ -0,0 +1,788 @@
1
+ """Protocol replay validation framework.
2
+
3
+ This module provides a framework for validating reverse-engineered protocols by
4
+ replaying messages to target devices and comparing expected vs actual responses.
5
+
6
+ Supports multiple interfaces:
7
+ - Serial ports (UART, RS-232, RS-485)
8
+ - SocketCAN (Controller Area Network)
9
+ - UDP/TCP sockets (network protocols)
10
+
11
+ Example:
12
+ >>> from oscura.validation.replay import ReplayConfig, ReplayValidator
13
+ >>> config = ReplayConfig(
14
+ ... interface="serial",
15
+ ... port="/dev/ttyUSB0",
16
+ ... baud_rate=115200,
17
+ ... timeout=1.0
18
+ ... )
19
+ >>> validator = ReplayValidator(config)
20
+ >>> spec = ProtocolSpec(
21
+ ... name="MyProtocol",
22
+ ... checksum_algorithm="crc16",
23
+ ... expected_response_time=0.1
24
+ ... )
25
+ >>> test_messages = [b"\\x01\\x02\\x03\\x04", b"\\x05\\x06\\x07\\x08"]
26
+ >>> result = validator.validate_protocol(spec, test_messages)
27
+ >>> print(f"Success: {result.success}, Messages: {result.messages_sent}")
28
+ Success: True, Messages: 2
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import time
34
+ from dataclasses import dataclass, field
35
+ from typing import TYPE_CHECKING, Any, Literal, Protocol
36
+
37
+ from oscura.utils.serial import connect_serial_port
38
+
39
+ if TYPE_CHECKING:
40
+ import socket
41
+
42
+
43
+ class CANBusProtocol(Protocol):
44
+ """Protocol for python-can Bus interface."""
45
+
46
+ def send(self, msg: Any) -> None:
47
+ """Send a CAN message."""
48
+ ...
49
+
50
+ def recv(self, timeout: float | None = None) -> Any:
51
+ """Receive a CAN message."""
52
+ ...
53
+
54
+
55
+ @dataclass
56
+ class ProtocolSpec:
57
+ """Protocol specification for validation.
58
+
59
+ Attributes:
60
+ name: Protocol name for identification.
61
+ checksum_algorithm: Checksum algorithm ("crc8", "crc16", "crc32", "xor", "sum").
62
+ checksum_position: Byte position of checksum in message (-1 for last byte).
63
+ expected_response_time: Expected response time in seconds.
64
+ timing_tolerance: Timing tolerance as fraction (0.1 = 10% tolerance).
65
+ require_response: Whether response is required for each message.
66
+ message_format: Message format description (optional).
67
+ """
68
+
69
+ name: str
70
+ checksum_algorithm: str = "none"
71
+ checksum_position: int = -1
72
+ expected_response_time: float = 0.1
73
+ timing_tolerance: float = 0.2
74
+ require_response: bool = True
75
+ message_format: str = ""
76
+
77
+
78
+ @dataclass
79
+ class ReplayConfig:
80
+ """Configuration for replay validation.
81
+
82
+ Attributes:
83
+ interface: Interface type ("serial", "socketcan", "udp", "tcp").
84
+ port: Port identifier ("/dev/ttyUSB0" for serial, "can0" for CAN, port number for UDP/TCP).
85
+ baud_rate: Baud rate for serial interface (default: 115200).
86
+ timeout: Timeout in seconds for receiving responses (default: 1.0).
87
+ validate_checksums: Enable checksum validation (default: True).
88
+ validate_timing: Enable timing validation (default: True).
89
+ max_retries: Maximum number of retries for failed sends (default: 3).
90
+ host: Host address for UDP/TCP (default: "localhost").
91
+ """
92
+
93
+ interface: Literal["serial", "socketcan", "udp", "tcp"]
94
+ port: str | int
95
+ baud_rate: int = 115200
96
+ timeout: float = 1.0
97
+ validate_checksums: bool = True
98
+ validate_timing: bool = True
99
+ max_retries: int = 3
100
+ host: str = "localhost"
101
+
102
+ def __post_init__(self) -> None:
103
+ """Validate configuration after initialization."""
104
+ if self.interface not in ("serial", "socketcan", "udp", "tcp"):
105
+ raise ValueError(
106
+ f"Invalid interface: {self.interface}. "
107
+ "Must be 'serial', 'socketcan', 'udp', or 'tcp'"
108
+ )
109
+ if self.timeout <= 0:
110
+ raise ValueError(f"timeout must be positive, got {self.timeout}")
111
+ if self.max_retries < 0:
112
+ raise ValueError(f"max_retries must be non-negative, got {self.max_retries}")
113
+ if self.baud_rate <= 0:
114
+ raise ValueError(f"baud_rate must be positive, got {self.baud_rate}")
115
+
116
+
117
+ @dataclass
118
+ class ValidationResult:
119
+ """Results from protocol validation.
120
+
121
+ Attributes:
122
+ success: Overall validation success status.
123
+ messages_sent: Number of messages successfully sent.
124
+ messages_received: Number of responses received.
125
+ checksum_valid: Number of valid checksums.
126
+ checksum_invalid: Number of invalid checksums.
127
+ timing_valid: Number of responses with valid timing.
128
+ timing_invalid: Number of responses with invalid timing.
129
+ errors: List of error messages encountered.
130
+ response_log: Detailed log of message/response pairs.
131
+ """
132
+
133
+ success: bool
134
+ messages_sent: int
135
+ messages_received: int
136
+ checksum_valid: int
137
+ checksum_invalid: int
138
+ timing_valid: int
139
+ timing_invalid: int
140
+ errors: list[str] = field(default_factory=list)
141
+ response_log: list[dict[str, Any]] = field(default_factory=list)
142
+
143
+ @property
144
+ def checksum_success_rate(self) -> float:
145
+ """Calculate checksum success rate.
146
+
147
+ Returns:
148
+ Fraction of checksums that were valid (0.0-1.0).
149
+ """
150
+ total = self.checksum_valid + self.checksum_invalid
151
+ return self.checksum_valid / total if total > 0 else 0.0
152
+
153
+ @property
154
+ def timing_success_rate(self) -> float:
155
+ """Calculate timing success rate.
156
+
157
+ Returns:
158
+ Fraction of responses with valid timing (0.0-1.0).
159
+ """
160
+ total = self.timing_valid + self.timing_invalid
161
+ return self.timing_valid / total if total > 0 else 0.0
162
+
163
+ @property
164
+ def response_rate(self) -> float:
165
+ """Calculate response rate.
166
+
167
+ Returns:
168
+ Fraction of messages that received responses (0.0-1.0).
169
+ """
170
+ return self.messages_received / self.messages_sent if self.messages_sent > 0 else 0.0
171
+
172
+
173
+ def _checksum_xor(data: bytes) -> int:
174
+ """Calculate XOR checksum.
175
+
176
+ Args:
177
+ data: Data bytes to checksum.
178
+
179
+ Returns:
180
+ XOR checksum (single byte).
181
+ """
182
+ result = 0
183
+ for byte in data:
184
+ result ^= byte
185
+ return result
186
+
187
+
188
+ def _checksum_sum(data: bytes) -> int:
189
+ """Calculate sum checksum.
190
+
191
+ Args:
192
+ data: Data bytes to checksum.
193
+
194
+ Returns:
195
+ Sum checksum (single byte, truncated).
196
+ """
197
+ return sum(data) & 0xFF
198
+
199
+
200
+ def _checksum_crc8(data: bytes) -> int:
201
+ """Calculate CRC-8 checksum (SAE J1850).
202
+
203
+ Args:
204
+ data: Data bytes to checksum.
205
+
206
+ Returns:
207
+ CRC-8 checksum.
208
+ """
209
+ crc = 0xFF
210
+ poly = 0x1D
211
+ for byte in data:
212
+ crc ^= byte
213
+ for _ in range(8):
214
+ if crc & 0x80:
215
+ crc = (crc << 1) ^ poly
216
+ else:
217
+ crc <<= 1
218
+ crc &= 0xFF
219
+ return crc ^ 0xFF
220
+
221
+
222
+ def _checksum_crc16(data: bytes) -> int:
223
+ """Calculate CRC-16 checksum (CCITT).
224
+
225
+ Args:
226
+ data: Data bytes to checksum.
227
+
228
+ Returns:
229
+ CRC-16 checksum low byte.
230
+ """
231
+ crc = 0xFFFF
232
+ poly = 0x1021
233
+ for byte in data:
234
+ crc ^= byte << 8
235
+ for _ in range(8):
236
+ if crc & 0x8000:
237
+ crc = (crc << 1) ^ poly
238
+ else:
239
+ crc <<= 1
240
+ crc &= 0xFFFF
241
+ return crc & 0xFF
242
+
243
+
244
+ def _init_validation_counters() -> dict[str, int]:
245
+ """Initialize validation counters.
246
+
247
+ Returns:
248
+ Dictionary with zero-initialized counters.
249
+ """
250
+ return {
251
+ "messages_sent": 0,
252
+ "messages_received": 0,
253
+ "checksum_valid": 0,
254
+ "checksum_invalid": 0,
255
+ "timing_valid": 0,
256
+ "timing_invalid": 0,
257
+ }
258
+
259
+
260
+ def _create_log_entry(index: int, message: bytes, send_time: float) -> dict[str, Any]:
261
+ """Create base log entry for message.
262
+
263
+ Args:
264
+ index: Message index.
265
+ message: Message bytes.
266
+ send_time: Send timestamp.
267
+
268
+ Returns:
269
+ Log entry dictionary.
270
+ """
271
+ return {
272
+ "message_index": index,
273
+ "message": message.hex(),
274
+ "send_time": send_time,
275
+ }
276
+
277
+
278
+ def _update_log_with_response(
279
+ log_entry: dict[str, Any], response: bytes, recv_time: float, send_time: float
280
+ ) -> None:
281
+ """Update log entry with response data.
282
+
283
+ Args:
284
+ log_entry: Log entry to update.
285
+ response: Response bytes.
286
+ recv_time: Receive timestamp.
287
+ send_time: Send timestamp.
288
+ """
289
+ log_entry["response"] = response.hex()
290
+ log_entry["recv_time"] = recv_time
291
+ log_entry["response_time"] = recv_time - send_time
292
+
293
+
294
+ def _validate_response(
295
+ validator: ReplayValidator,
296
+ response: bytes,
297
+ spec: ProtocolSpec,
298
+ index: int,
299
+ recv_time: float,
300
+ send_time: float,
301
+ counters: dict[str, int],
302
+ errors: list[str],
303
+ log_entry: dict[str, Any],
304
+ ) -> None:
305
+ """Validate response checksum and timing.
306
+
307
+ Args:
308
+ validator: ReplayValidator instance.
309
+ response: Response bytes.
310
+ spec: Protocol specification.
311
+ index: Message index.
312
+ recv_time: Receive timestamp.
313
+ send_time: Send timestamp.
314
+ counters: Validation counters.
315
+ errors: Error list.
316
+ log_entry: Log entry to update.
317
+ """
318
+ # Validate checksum if enabled
319
+ if validator.config.validate_checksums and spec.checksum_algorithm != "none":
320
+ if validator._validate_checksum(response, spec):
321
+ counters["checksum_valid"] += 1
322
+ log_entry["checksum_valid"] = True
323
+ else:
324
+ counters["checksum_invalid"] += 1
325
+ log_entry["checksum_valid"] = False
326
+ errors.append(f"Message {index}: Invalid checksum in response")
327
+
328
+ # Validate timing if enabled
329
+ if validator.config.validate_timing:
330
+ response_time = recv_time - send_time
331
+ if validator._validate_timing(
332
+ send_time, recv_time, spec.expected_response_time, spec.timing_tolerance
333
+ ):
334
+ counters["timing_valid"] += 1
335
+ log_entry["timing_valid"] = True
336
+ else:
337
+ counters["timing_invalid"] += 1
338
+ log_entry["timing_valid"] = False
339
+ errors.append(f"Message {index}: Response time {response_time:.3f}s outside tolerance")
340
+
341
+
342
+ def _compute_overall_success(
343
+ errors: list[str], counters: dict[str, int], spec: ProtocolSpec
344
+ ) -> bool:
345
+ """Compute overall validation success.
346
+
347
+ Args:
348
+ errors: List of errors.
349
+ counters: Validation counters.
350
+ spec: Protocol specification.
351
+
352
+ Returns:
353
+ True if validation successful.
354
+ """
355
+ return (
356
+ len(errors) == 0
357
+ and counters["messages_sent"] > 0
358
+ and (
359
+ not spec.require_response or counters["messages_received"] == counters["messages_sent"]
360
+ )
361
+ )
362
+
363
+
364
+ def _checksum_crc32(data: bytes) -> int:
365
+ """Calculate CRC-32 checksum (IEEE 802.3).
366
+
367
+ Args:
368
+ data: Data bytes to checksum.
369
+
370
+ Returns:
371
+ CRC-32 checksum low byte.
372
+ """
373
+ crc = 0xFFFFFFFF
374
+ poly = 0xEDB88320
375
+ for byte in data:
376
+ crc ^= byte
377
+ for _ in range(8):
378
+ if crc & 1:
379
+ crc = (crc >> 1) ^ poly
380
+ else:
381
+ crc >>= 1
382
+ return (~crc) & 0xFF
383
+
384
+
385
+ class ReplayValidator:
386
+ """Validator for protocol replay testing.
387
+
388
+ Sends test messages to target devices via serial, CAN, or network interfaces,
389
+ captures responses, and validates checksums, timing, and state transitions.
390
+
391
+ Example:
392
+ >>> config = ReplayConfig(interface="serial", port="/dev/ttyUSB0")
393
+ >>> validator = ReplayValidator(config)
394
+ >>> spec = ProtocolSpec(name="UART", checksum_algorithm="xor")
395
+ >>> result = validator.validate_protocol(spec, [b"\\x01\\x02\\x03"])
396
+ >>> validator.close()
397
+ """
398
+
399
+ def __init__(self, config: ReplayConfig) -> None:
400
+ """Initialize validator with interface configuration.
401
+
402
+ Args:
403
+ config: Replay configuration specifying interface and parameters.
404
+
405
+ Raises:
406
+ ImportError: If required library for interface is not installed.
407
+ ValueError: If configuration is invalid.
408
+ """
409
+ self.config = config
410
+ self._connection: Any = None
411
+ self._is_connected = False
412
+
413
+ def __enter__(self) -> ReplayValidator:
414
+ """Context manager entry."""
415
+ self.connect()
416
+ return self
417
+
418
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
419
+ """Context manager exit."""
420
+ self.close()
421
+
422
+ def connect(self) -> None:
423
+ """Establish connection to target device.
424
+
425
+ Raises:
426
+ ImportError: If required library is not installed.
427
+ OSError: If connection fails.
428
+ """
429
+ if self._is_connected:
430
+ return
431
+
432
+ if self.config.interface == "serial":
433
+ self._connect_serial()
434
+ elif self.config.interface == "socketcan":
435
+ self._connect_socketcan()
436
+ elif self.config.interface in ("udp", "tcp"):
437
+ self._connect_network()
438
+
439
+ self._is_connected = True
440
+
441
+ def _connect_serial(self) -> None:
442
+ """Connect to serial port.
443
+
444
+ Raises:
445
+ ValueError: If port is not a string.
446
+ ImportError: If pyserial is not installed.
447
+ OSError: If serial port cannot be opened.
448
+ """
449
+ if not isinstance(self.config.port, str):
450
+ raise ValueError("Serial port must be string")
451
+ self._connection = connect_serial_port(
452
+ port=self.config.port,
453
+ baud_rate=self.config.baud_rate,
454
+ timeout=self.config.timeout,
455
+ )
456
+
457
+ def _connect_socketcan(self) -> None:
458
+ """Connect to SocketCAN interface.
459
+
460
+ Raises:
461
+ ImportError: If python-can is not installed.
462
+ OSError: If CAN interface cannot be opened.
463
+ """
464
+ try:
465
+ import can
466
+ except ImportError as e:
467
+ raise ImportError(
468
+ "python-can is required for SocketCAN interface. "
469
+ "Install with: pip install python-can"
470
+ ) from e
471
+
472
+ if not isinstance(self.config.port, str):
473
+ raise ValueError(f"CAN interface must be string, got {type(self.config.port)}")
474
+
475
+ self._connection = can.interface.Bus(
476
+ channel=self.config.port, interface="socketcan", receive_own_messages=False
477
+ )
478
+
479
+ def _connect_network(self) -> None:
480
+ """Connect to UDP/TCP socket.
481
+
482
+ Raises:
483
+ OSError: If socket cannot be created or connected.
484
+ """
485
+ import socket
486
+
487
+ if not isinstance(self.config.port, int):
488
+ raise ValueError(f"Network port must be integer, got {type(self.config.port)}")
489
+
490
+ if self.config.interface == "udp":
491
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
492
+ sock.settimeout(self.config.timeout)
493
+ else: # tcp
494
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
495
+ sock.settimeout(self.config.timeout)
496
+ sock.connect((self.config.host, self.config.port))
497
+
498
+ self._connection = sock
499
+
500
+ def close(self) -> None:
501
+ """Close connection to target device."""
502
+ if not self._is_connected or self._connection is None:
503
+ return
504
+
505
+ try:
506
+ if hasattr(self._connection, "close"):
507
+ self._connection.close()
508
+ elif hasattr(self._connection, "shutdown"):
509
+ self._connection.shutdown()
510
+ finally:
511
+ self._connection = None
512
+ self._is_connected = False
513
+
514
+ def validate_protocol(self, spec: ProtocolSpec, test_messages: list[bytes]) -> ValidationResult:
515
+ """Send test messages and validate responses.
516
+
517
+ Args:
518
+ spec: Protocol specification with validation criteria.
519
+ test_messages: List of test messages to send.
520
+
521
+ Returns:
522
+ Validation result with success status and detailed metrics.
523
+
524
+ Raises:
525
+ RuntimeError: If not connected to target device.
526
+
527
+ Example:
528
+ >>> config = ReplayConfig(interface="serial", port="/dev/ttyUSB0")
529
+ >>> validator = ReplayValidator(config)
530
+ >>> validator.connect()
531
+ >>> spec = ProtocolSpec(name="Test", checksum_algorithm="xor")
532
+ >>> result = validator.validate_protocol(spec, [b"\\x01\\x02\\x03"])
533
+ >>> print(f"Success: {result.success}")
534
+ >>> validator.close()
535
+ """
536
+ if not self._is_connected:
537
+ raise RuntimeError("Not connected. Call connect() first.")
538
+
539
+ counters = _init_validation_counters()
540
+ errors: list[str] = []
541
+ response_log: list[dict[str, Any]] = []
542
+
543
+ for i, message in enumerate(test_messages):
544
+ try:
545
+ send_time, response, recv_time = self._send_and_measure(message)
546
+ counters["messages_sent"] += 1
547
+
548
+ log_entry = _create_log_entry(i, message, send_time)
549
+
550
+ if response is not None:
551
+ counters["messages_received"] += 1
552
+ _update_log_with_response(log_entry, response, recv_time, send_time)
553
+ _validate_response(
554
+ self, response, spec, i, recv_time, send_time, counters, errors, log_entry
555
+ )
556
+ else:
557
+ log_entry["response"] = None
558
+ if spec.require_response:
559
+ errors.append(f"Message {i}: No response received")
560
+
561
+ response_log.append(log_entry)
562
+
563
+ except Exception as e:
564
+ errors.append(f"Message {i}: {type(e).__name__}: {e}")
565
+ response_log.append({"message_index": i, "message": message.hex(), "error": str(e)})
566
+
567
+ success = _compute_overall_success(errors, counters, spec)
568
+
569
+ return ValidationResult(
570
+ success=success,
571
+ messages_sent=counters["messages_sent"],
572
+ messages_received=counters["messages_received"],
573
+ checksum_valid=counters["checksum_valid"],
574
+ checksum_invalid=counters["checksum_invalid"],
575
+ timing_valid=counters["timing_valid"],
576
+ timing_invalid=counters["timing_invalid"],
577
+ errors=errors,
578
+ response_log=response_log,
579
+ )
580
+
581
+ def _send_and_measure(self, message: bytes) -> tuple[float, bytes | None, float]:
582
+ """Send message and measure timing.
583
+
584
+ Args:
585
+ message: Message bytes to send.
586
+
587
+ Returns:
588
+ Tuple of (send_time, response, recv_time).
589
+ """
590
+ send_time = time.time()
591
+ response = self._send_message(message)
592
+ recv_time = time.time()
593
+ return send_time, response, recv_time
594
+
595
+ def _send_message(self, message: bytes) -> bytes | None:
596
+ """Send message and capture response.
597
+
598
+ Args:
599
+ message: Message bytes to send.
600
+
601
+ Returns:
602
+ Response bytes, or None if no response received.
603
+
604
+ Raises:
605
+ OSError: If send/receive fails.
606
+ """
607
+ if self.config.interface == "serial":
608
+ return self._send_serial(message)
609
+ elif self.config.interface == "socketcan":
610
+ return self._send_socketcan(message)
611
+ elif self.config.interface in ("udp", "tcp"):
612
+ return self._send_network(message)
613
+
614
+ return None
615
+
616
+ def _send_serial(self, message: bytes) -> bytes | None:
617
+ """Send message via serial port.
618
+
619
+ Args:
620
+ message: Message bytes to send.
621
+
622
+ Returns:
623
+ Response bytes, or None if timeout.
624
+ """
625
+ import serial # type: ignore[import-untyped]
626
+
627
+ ser: serial.Serial = self._connection
628
+ ser.reset_input_buffer() # Clear any old data
629
+ ser.write(message)
630
+ ser.flush()
631
+
632
+ # Read response with timeout
633
+ response = ser.read(1024) # Read up to 1KB
634
+ return response if response else None
635
+
636
+ def _send_socketcan(self, message: bytes) -> bytes | None:
637
+ """Send message via SocketCAN.
638
+
639
+ Args:
640
+ message: Message bytes to send (max 8 bytes for CAN 2.0, 64 for CAN-FD).
641
+
642
+ Returns:
643
+ Response bytes, or None if timeout.
644
+ """
645
+ import can
646
+
647
+ bus: CANBusProtocol = self._connection
648
+
649
+ # Create CAN message with standard ID 0x123 (configurable in future)
650
+ msg = can.Message(arbitration_id=0x123, data=message, is_extended_id=False)
651
+ bus.send(msg)
652
+
653
+ # Wait for response
654
+ response_msg = bus.recv(timeout=self.config.timeout)
655
+ return bytes(response_msg.data) if response_msg else None
656
+
657
+ def _send_network(self, message: bytes) -> bytes | None:
658
+ """Send message via UDP/TCP socket.
659
+
660
+ Args:
661
+ message: Message bytes to send.
662
+
663
+ Returns:
664
+ Response bytes, or None if timeout.
665
+ """
666
+
667
+ sock: socket.socket = self._connection
668
+
669
+ if self.config.interface == "udp":
670
+ sock.sendto(message, (self.config.host, self.config.port))
671
+ try:
672
+ response, _ = sock.recvfrom(1024)
673
+ return response
674
+ except TimeoutError:
675
+ return None
676
+ else: # tcp
677
+ sock.sendall(message)
678
+ try:
679
+ response = sock.recv(1024)
680
+ return response if response else None
681
+ except TimeoutError:
682
+ return None
683
+
684
+ def _validate_checksum(self, message: bytes, spec: ProtocolSpec) -> bool:
685
+ """Verify checksum in received message.
686
+
687
+ Args:
688
+ message: Message bytes to validate.
689
+ spec: Protocol specification with checksum configuration.
690
+
691
+ Returns:
692
+ True if checksum is valid, False otherwise.
693
+
694
+ Example:
695
+ >>> validator = ReplayValidator(ReplayConfig("serial", "/dev/null"))
696
+ >>> spec = ProtocolSpec(name="Test", checksum_algorithm="xor")
697
+ >>> validator._validate_checksum(b"\\x01\\x02\\x03", spec)
698
+ True
699
+ """
700
+ if len(message) == 0:
701
+ return False
702
+
703
+ # Extract checksum from message
704
+ checksum_pos = spec.checksum_position if spec.checksum_position >= 0 else len(message) - 1
705
+ if checksum_pos >= len(message):
706
+ return False
707
+
708
+ received_checksum = message[checksum_pos]
709
+
710
+ # Calculate expected checksum on data portion
711
+ if checksum_pos == len(message) - 1:
712
+ data = message[:-1]
713
+ else:
714
+ data = message[:checksum_pos] + message[checksum_pos + 1 :]
715
+
716
+ expected_checksum = self._calculate_checksum(data, spec.checksum_algorithm)
717
+
718
+ return received_checksum == expected_checksum
719
+
720
+ def _calculate_checksum(self, data: bytes, algorithm: str) -> int:
721
+ """Calculate checksum using specified algorithm.
722
+
723
+ Args:
724
+ data: Data bytes to checksum.
725
+ algorithm: Checksum algorithm ("crc8", "crc16", "crc32", "xor", "sum").
726
+
727
+ Returns:
728
+ Calculated checksum value (truncated to single byte for most algorithms).
729
+
730
+ Raises:
731
+ ValueError: If algorithm is not supported.
732
+
733
+ Example:
734
+ >>> validator = ReplayValidator(ReplayConfig("serial", "/dev/null"))
735
+ >>> validator._calculate_checksum(b"\\x01\\x02\\x03", "xor")
736
+ 0
737
+ """
738
+ checksum_functions = {
739
+ "xor": _checksum_xor,
740
+ "sum": _checksum_sum,
741
+ "crc8": _checksum_crc8,
742
+ "crc16": _checksum_crc16,
743
+ "crc32": _checksum_crc32,
744
+ }
745
+
746
+ if algorithm not in checksum_functions:
747
+ raise ValueError(
748
+ f"Unsupported checksum algorithm: {algorithm}. "
749
+ f"Supported: {', '.join(checksum_functions.keys())}"
750
+ )
751
+
752
+ return checksum_functions[algorithm](data)
753
+
754
+ def _validate_timing(
755
+ self, send_time: float, recv_time: float, expected: float, tolerance: float
756
+ ) -> bool:
757
+ """Verify response timing within tolerance.
758
+
759
+ Args:
760
+ send_time: Message send timestamp.
761
+ recv_time: Response receive timestamp.
762
+ expected: Expected response time in seconds.
763
+ tolerance: Timing tolerance as fraction (0.1 = 10%).
764
+
765
+ Returns:
766
+ True if timing is within tolerance, False otherwise.
767
+
768
+ Example:
769
+ >>> validator = ReplayValidator(ReplayConfig("serial", "/dev/null"))
770
+ >>> validator._validate_timing(0.0, 0.1, 0.1, 0.2)
771
+ True
772
+ >>> validator._validate_timing(0.0, 0.2, 0.1, 0.2)
773
+ False
774
+ """
775
+ actual_time = recv_time - send_time
776
+ lower_bound = expected * (1.0 - tolerance)
777
+ upper_bound = expected * (1.0 + tolerance)
778
+ # Add small epsilon to handle floating point precision errors
779
+ eps = 1e-9
780
+ return (lower_bound - eps) <= actual_time <= (upper_bound + eps)
781
+
782
+
783
+ __all__ = [
784
+ "ProtocolSpec",
785
+ "ReplayConfig",
786
+ "ReplayValidator",
787
+ "ValidationResult",
788
+ ]