oscura 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (513) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
+ ]