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,977 @@
1
+ """Hardware-in-Loop (HIL) testing framework for real hardware validation.
2
+
3
+ This module provides comprehensive HIL testing capabilities for validating protocol
4
+ implementations against real hardware using various interface types.
5
+
6
+ Supports multiple hardware interfaces:
7
+ - Serial ports (UART, RS-232, RS-485) via pyserial
8
+ - SocketCAN for automotive/embedded CAN testing via python-can
9
+ - USB devices via pyusb
10
+ - SPI/I2C via spidev/smbus (Linux only)
11
+ - GPIO control via RPi.GPIO or gpiod
12
+ - Optional oscilloscope integration via PyVISA
13
+
14
+ Example:
15
+ >>> from oscura.validation.hil_testing import HILTester, HILConfig
16
+ >>> config = HILConfig(
17
+ ... interface="serial",
18
+ ... port="/dev/ttyUSB0",
19
+ ... baud_rate=115200,
20
+ ... reset_gpio=17
21
+ ... )
22
+ >>> tester = HILTester(config)
23
+ >>> test_cases = [
24
+ ... {"name": "ping", "send": b"\\x01\\x02", "expect": b"\\x03\\x04", "timeout": 0.5}
25
+ ... ]
26
+ >>> report = tester.run_tests(test_cases)
27
+ >>> print(f"Passed: {report.passed}/{report.total}")
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import time
33
+ from dataclasses import dataclass, field
34
+ from enum import Enum
35
+ from typing import Any, Protocol
36
+
37
+ try:
38
+ import can # type: ignore[import-untyped]
39
+ except ImportError:
40
+ can = None # type: ignore[assignment]
41
+
42
+ try:
43
+ import usb # type: ignore[import-not-found]
44
+ import usb.core # type: ignore[import-not-found]
45
+ except ImportError:
46
+ # Create module structure for test patching even when pyusb unavailable
47
+ import types
48
+
49
+ usb = types.ModuleType("usb") # type: ignore[assignment]
50
+ usb.core = None # type: ignore[attr-defined]
51
+
52
+ try:
53
+ import spidev # type: ignore[import-not-found]
54
+ except ImportError:
55
+ spidev = None # type: ignore[assignment]
56
+
57
+ try:
58
+ from smbus2 import SMBus # type: ignore[import-not-found]
59
+ except ImportError:
60
+ SMBus = None # type: ignore[assignment]
61
+
62
+ try:
63
+ import RPi.GPIO as GPIO # type: ignore[import-untyped]
64
+ except ImportError:
65
+ try:
66
+ import gpiod # type: ignore[import-not-found]
67
+
68
+ GPIO = None # type: ignore[assignment]
69
+ except ImportError:
70
+ GPIO = None # type: ignore[assignment]
71
+ gpiod = None # type: ignore[assignment]
72
+
73
+ try:
74
+ from scapy.all import IP, UDP, Packet, wrpcap # type: ignore[attr-defined]
75
+ except ImportError:
76
+ IP = None # type: ignore[assignment]
77
+ UDP = None # type: ignore[assignment]
78
+ Packet = None # type: ignore[assignment,misc]
79
+ wrpcap = None # type: ignore[assignment]
80
+
81
+ try:
82
+ import serial # type: ignore[import-untyped]
83
+ except ImportError:
84
+ serial = None # type: ignore[assignment]
85
+
86
+ from oscura.utils.serial import connect_serial_port
87
+
88
+
89
+ class CANBusProtocol(Protocol):
90
+ """Protocol for python-can Bus interface."""
91
+
92
+ def send(self, msg: Any) -> None:
93
+ """Send a CAN message."""
94
+ ...
95
+
96
+ def recv(self, timeout: float | None = None) -> Any:
97
+ """Receive a CAN message."""
98
+ ...
99
+
100
+
101
+ class InterfaceType(str, Enum):
102
+ """Supported hardware interface types."""
103
+
104
+ SERIAL = "serial"
105
+ SOCKETCAN = "socketcan"
106
+ USB = "usb"
107
+ SPI = "spi"
108
+ I2C = "i2c"
109
+
110
+
111
+ class TestStatus(str, Enum):
112
+ """Test execution status."""
113
+
114
+ PASSED = "passed"
115
+ FAILED = "failed"
116
+ ERROR = "error"
117
+ TIMEOUT = "timeout"
118
+ SKIPPED = "skipped"
119
+
120
+
121
+ @dataclass
122
+ class HILConfig:
123
+ """Configuration for Hardware-in-Loop testing.
124
+
125
+ Attributes:
126
+ interface: Interface type (serial, socketcan, usb, spi, i2c).
127
+ port: Port identifier (e.g., "/dev/ttyUSB0", "can0", spi device number).
128
+ baud_rate: Baud rate for serial interface (default: 115200).
129
+ timeout: Default timeout in seconds for responses (default: 1.0).
130
+ reset_gpio: GPIO pin number for device reset (optional).
131
+ power_gpio: GPIO pin number for device power control (optional).
132
+ reset_duration: Reset pulse duration in seconds (default: 0.1).
133
+ setup_delay: Delay after setup before testing in seconds (default: 0.5).
134
+ teardown_delay: Delay before teardown in seconds (default: 0.1).
135
+ dry_run: Enable dry-run mode without real hardware (default: False).
136
+ validate_timing: Enable timing validation (default: True).
137
+ capture_pcap: Enable PCAP capture of traffic (default: False).
138
+ pcap_file: Output PCAP file path (default: "hil_capture.pcap").
139
+ oscilloscope_address: VISA address for oscilloscope (optional).
140
+ usb_vendor_id: USB vendor ID (for USB interface).
141
+ usb_product_id: USB product ID (for USB interface).
142
+ spi_bus: SPI bus number (default: 0).
143
+ spi_device: SPI device number (default: 0).
144
+ spi_speed_hz: SPI clock speed in Hz (default: 1000000).
145
+ i2c_bus: I2C bus number (default: 1).
146
+ i2c_address: I2C device address (default: 0x50).
147
+ """
148
+
149
+ interface: InterfaceType | str
150
+ port: str | int
151
+ baud_rate: int = 115200
152
+ timeout: float = 1.0
153
+ reset_gpio: int | None = None
154
+ power_gpio: int | None = None
155
+ reset_duration: float = 0.1
156
+ setup_delay: float = 0.5
157
+ teardown_delay: float = 0.1
158
+ dry_run: bool = False
159
+ validate_timing: bool = True
160
+ capture_pcap: bool = False
161
+ pcap_file: str = "hil_capture.pcap"
162
+ oscilloscope_address: str | None = None
163
+ usb_vendor_id: int | None = None
164
+ usb_product_id: int | None = None
165
+ spi_bus: int = 0
166
+ spi_device: int = 0
167
+ spi_speed_hz: int = 1000000
168
+ i2c_bus: int = 1
169
+ i2c_address: int = 0x50
170
+
171
+ def __post_init__(self) -> None:
172
+ """Validate configuration after initialization."""
173
+ # Convert string to enum if needed
174
+ if isinstance(self.interface, str):
175
+ try:
176
+ self.interface = InterfaceType(self.interface)
177
+ except ValueError as e:
178
+ raise ValueError(
179
+ f"Invalid interface: {self.interface}. "
180
+ f"Must be one of: {', '.join(t.value for t in InterfaceType)}"
181
+ ) from e
182
+
183
+ if self.timeout <= 0:
184
+ raise ValueError(f"timeout must be positive, got {self.timeout}")
185
+ if self.baud_rate <= 0:
186
+ raise ValueError(f"baud_rate must be positive, got {self.baud_rate}")
187
+ if self.reset_duration < 0:
188
+ raise ValueError(f"reset_duration must be non-negative, got {self.reset_duration}")
189
+ if self.setup_delay < 0:
190
+ raise ValueError(f"setup_delay must be non-negative, got {self.setup_delay}")
191
+
192
+
193
+ @dataclass
194
+ class HILTestResult:
195
+ """Result from a single HIL test case.
196
+
197
+ Attributes:
198
+ test_name: Name of the test case.
199
+ status: Test execution status (passed, failed, error, timeout, skipped).
200
+ sent_data: Data sent to hardware (as hex string).
201
+ received_data: Data received from hardware (as hex string, None if timeout).
202
+ expected_data: Expected response data (as hex string, None if not specified).
203
+ latency: Response latency in seconds (None if timeout).
204
+ error: Error message if status is ERROR (None otherwise).
205
+ timestamp: Test execution timestamp.
206
+ timing_valid: Whether timing was within tolerance (None if not validated).
207
+ bit_errors: Number of bit errors detected (0 if perfect match).
208
+ """
209
+
210
+ test_name: str
211
+ status: TestStatus
212
+ sent_data: str
213
+ received_data: str | None
214
+ expected_data: str | None
215
+ latency: float | None
216
+ error: str | None = None
217
+ timestamp: float = field(default_factory=time.time)
218
+ timing_valid: bool | None = None
219
+ bit_errors: int = 0
220
+
221
+ @property
222
+ def passed(self) -> bool:
223
+ """Check if test passed."""
224
+ return self.status == TestStatus.PASSED
225
+
226
+
227
+ @dataclass
228
+ class HILTestReport:
229
+ """Comprehensive report from HIL test execution.
230
+
231
+ Attributes:
232
+ test_results: List of individual test results.
233
+ total: Total number of tests executed.
234
+ passed: Number of tests that passed.
235
+ failed: Number of tests that failed.
236
+ errors: Number of tests with errors.
237
+ timeouts: Number of tests that timed out.
238
+ skipped: Number of tests that were skipped.
239
+ hardware_info: Hardware configuration information.
240
+ timing_statistics: Timing statistics (min/max/avg latency in seconds).
241
+ start_time: Test suite start timestamp.
242
+ end_time: Test suite end timestamp.
243
+ duration: Total execution duration in seconds.
244
+ """
245
+
246
+ test_results: list[HILTestResult]
247
+ total: int
248
+ passed: int
249
+ failed: int
250
+ errors: int
251
+ timeouts: int
252
+ skipped: int
253
+ hardware_info: dict[str, Any]
254
+ timing_statistics: dict[str, float]
255
+ start_time: float
256
+ end_time: float
257
+ duration: float
258
+
259
+ @property
260
+ def success_rate(self) -> float:
261
+ """Calculate overall success rate.
262
+
263
+ Returns:
264
+ Success rate as fraction (0.0-1.0).
265
+ """
266
+ return self.passed / self.total if self.total > 0 else 0.0
267
+
268
+ def to_dict(self) -> dict[str, Any]:
269
+ """Export report to dictionary for JSON serialization.
270
+
271
+ Returns:
272
+ Dictionary with complete test report data.
273
+ """
274
+ return {
275
+ "test_results": [
276
+ {
277
+ "test_name": r.test_name,
278
+ "status": r.status.value,
279
+ "sent_data": r.sent_data,
280
+ "received_data": r.received_data,
281
+ "expected_data": r.expected_data,
282
+ "latency": r.latency,
283
+ "error": r.error,
284
+ "timestamp": r.timestamp,
285
+ "timing_valid": r.timing_valid,
286
+ "bit_errors": r.bit_errors,
287
+ }
288
+ for r in self.test_results
289
+ ],
290
+ "summary": {
291
+ "total": self.total,
292
+ "passed": self.passed,
293
+ "failed": self.failed,
294
+ "errors": self.errors,
295
+ "timeouts": self.timeouts,
296
+ "skipped": self.skipped,
297
+ "success_rate": self.success_rate,
298
+ },
299
+ "hardware_info": self.hardware_info,
300
+ "timing_statistics": self.timing_statistics,
301
+ "start_time": self.start_time,
302
+ "end_time": self.end_time,
303
+ "duration": self.duration,
304
+ }
305
+
306
+
307
+ class HILTester:
308
+ """Hardware-in-Loop testing framework.
309
+
310
+ Automates hardware validation testing by sending test vectors to real hardware
311
+ and validating responses against expected behavior. Supports multiple interface
312
+ types with automatic setup/teardown.
313
+
314
+ Example:
315
+ >>> config = HILConfig(interface="serial", port="/dev/ttyUSB0")
316
+ >>> tester = HILTester(config)
317
+ >>> tester.setup()
318
+ >>> test = {"name": "echo", "send": b"\\x01", "expect": b"\\x01"}
319
+ >>> result = tester.run_test(test)
320
+ >>> tester.teardown()
321
+ """
322
+
323
+ def __init__(self, config: HILConfig) -> None:
324
+ """Initialize HIL tester with configuration.
325
+
326
+ Args:
327
+ config: HIL testing configuration.
328
+ """
329
+ self.config = config
330
+ self._connection: Any = None
331
+ self._gpio_controller: Any = None
332
+ self._is_setup = False
333
+ self._pcap_packets: list[tuple[float, bytes, bytes | None]] = []
334
+
335
+ def __enter__(self) -> HILTester:
336
+ """Context manager entry - setup hardware."""
337
+ self.setup()
338
+ return self
339
+
340
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
341
+ """Context manager exit - teardown hardware."""
342
+ self.teardown()
343
+
344
+ def setup(self) -> None:
345
+ """Setup hardware connection and initialize device.
346
+
347
+ Performs:
348
+ 1. GPIO initialization (if configured)
349
+ 2. Power on device (if power GPIO configured)
350
+ 3. Reset device (if reset GPIO configured)
351
+ 4. Initialize interface connection
352
+ 5. Wait for device stability
353
+
354
+ Raises:
355
+ ImportError: If required library is not installed.
356
+ OSError: If hardware connection fails.
357
+ RuntimeError: If already setup.
358
+ """
359
+ if self._is_setup:
360
+ raise RuntimeError("Already setup. Call teardown() first.")
361
+
362
+ # Initialize GPIO if needed
363
+ if self.config.reset_gpio is not None or self.config.power_gpio is not None:
364
+ self._setup_gpio()
365
+
366
+ # Power on device if configured
367
+ if self.config.power_gpio is not None:
368
+ self._power_on()
369
+
370
+ # Reset device if configured
371
+ if self.config.reset_gpio is not None:
372
+ self._reset_device()
373
+
374
+ # Connect to hardware interface
375
+ if not self.config.dry_run:
376
+ self._connect()
377
+
378
+ # Wait for device to stabilize
379
+ if self.config.setup_delay > 0:
380
+ time.sleep(self.config.setup_delay)
381
+
382
+ self._is_setup = True
383
+
384
+ def teardown(self) -> None:
385
+ """Teardown hardware connection and cleanup resources.
386
+
387
+ Performs:
388
+ 1. Wait for teardown delay
389
+ 2. Close interface connection
390
+ 3. Power off device (if power GPIO configured)
391
+ 4. Cleanup GPIO resources
392
+ 5. Export PCAP if capture enabled
393
+ """
394
+ if not self._is_setup:
395
+ return
396
+
397
+ # Wait before teardown
398
+ if self.config.teardown_delay > 0:
399
+ time.sleep(self.config.teardown_delay)
400
+
401
+ # Close connection
402
+ if self._connection is not None:
403
+ try:
404
+ if hasattr(self._connection, "close"):
405
+ self._connection.close()
406
+ elif hasattr(self._connection, "shutdown"):
407
+ self._connection.shutdown()
408
+ except Exception:
409
+ pass # Best effort cleanup
410
+ finally:
411
+ self._connection = None
412
+
413
+ # Power off device if configured
414
+ if self.config.power_gpio is not None and self._gpio_controller is not None:
415
+ self._power_off()
416
+
417
+ # Cleanup GPIO
418
+ if self._gpio_controller is not None:
419
+ try:
420
+ if hasattr(self._gpio_controller, "cleanup"):
421
+ self._gpio_controller.cleanup()
422
+ except Exception:
423
+ pass
424
+ finally:
425
+ self._gpio_controller = None
426
+
427
+ # Export PCAP if enabled
428
+ if self.config.capture_pcap and self._pcap_packets:
429
+ self._export_pcap()
430
+
431
+ self._is_setup = False
432
+
433
+ def run_test(
434
+ self,
435
+ test_case: dict[str, Any],
436
+ ) -> HILTestResult:
437
+ """Execute a single test case.
438
+
439
+ Args:
440
+ test_case: Test case dictionary with keys:
441
+ - name (str): Test case name
442
+ - send (bytes): Data to send to hardware
443
+ - expect (bytes, optional): Expected response data
444
+ - timeout (float, optional): Override default timeout
445
+ - max_latency (float, optional): Maximum acceptable latency
446
+ - min_latency (float, optional): Minimum acceptable latency
447
+ - skip (bool, optional): Skip this test
448
+
449
+ Returns:
450
+ Test result with status, timing, and validation info.
451
+
452
+ Raises:
453
+ RuntimeError: If not setup.
454
+
455
+ Example:
456
+ >>> tester = HILTester(HILConfig("serial", "/dev/ttyUSB0"))
457
+ >>> tester.setup()
458
+ >>> result = tester.run_test({
459
+ ... "name": "echo_test",
460
+ ... "send": b"\\x01\\x02",
461
+ ... "expect": b"\\x01\\x02",
462
+ ... "timeout": 0.5
463
+ ... })
464
+ >>> tester.teardown()
465
+ """
466
+ if not self._is_setup:
467
+ raise RuntimeError("Not setup. Call setup() first.")
468
+
469
+ test_params = self._extract_test_parameters(test_case)
470
+ if test_params["skip"]:
471
+ return self._create_skipped_result(test_params)
472
+
473
+ try:
474
+ return self._execute_test_case(test_params)
475
+ except Exception as e:
476
+ return self._create_error_result(test_params, e)
477
+
478
+ def _extract_test_parameters(self, test_case: dict[str, Any]) -> dict[str, Any]:
479
+ """Extract test parameters from test case dictionary."""
480
+ return {
481
+ "test_name": test_case.get("name", "unnamed_test"),
482
+ "send_data": test_case.get("send", b""),
483
+ "expect_data": test_case.get("expect"),
484
+ "timeout": test_case.get("timeout", self.config.timeout),
485
+ "max_latency": test_case.get("max_latency"),
486
+ "min_latency": test_case.get("min_latency"),
487
+ "skip": test_case.get("skip", False),
488
+ }
489
+
490
+ def _create_skipped_result(self, test_params: dict[str, Any]) -> HILTestResult:
491
+ """Create result for skipped test."""
492
+ return HILTestResult(
493
+ test_name=test_params["test_name"],
494
+ status=TestStatus.SKIPPED,
495
+ sent_data=test_params["send_data"].hex(),
496
+ received_data=None,
497
+ expected_data=test_params["expect_data"].hex() if test_params["expect_data"] else None,
498
+ latency=None,
499
+ )
500
+
501
+ def _execute_test_case(self, test_params: dict[str, Any]) -> HILTestResult:
502
+ """Execute test case and return result."""
503
+ start_time = time.time()
504
+ response = self._send_receive(test_params["send_data"], test_params["timeout"])
505
+ end_time = time.time()
506
+ latency = end_time - start_time
507
+
508
+ if self.config.capture_pcap:
509
+ self._pcap_packets.append((start_time, test_params["send_data"], response))
510
+
511
+ if response is None:
512
+ return self._create_timeout_result(test_params)
513
+
514
+ status, bit_errors = self._evaluate_response(response, test_params["expect_data"])
515
+ timing_valid = self._validate_timing(
516
+ latency, test_params["max_latency"], test_params["min_latency"], status
517
+ )
518
+
519
+ if timing_valid is False and status == TestStatus.PASSED:
520
+ status = TestStatus.FAILED
521
+
522
+ return HILTestResult(
523
+ test_name=test_params["test_name"],
524
+ status=status,
525
+ sent_data=test_params["send_data"].hex(),
526
+ received_data=response.hex(),
527
+ expected_data=test_params["expect_data"].hex() if test_params["expect_data"] else None,
528
+ latency=latency,
529
+ timing_valid=timing_valid,
530
+ bit_errors=bit_errors,
531
+ )
532
+
533
+ def _create_timeout_result(self, test_params: dict[str, Any]) -> HILTestResult:
534
+ """Create result for timed-out test."""
535
+ return HILTestResult(
536
+ test_name=test_params["test_name"],
537
+ status=TestStatus.TIMEOUT,
538
+ sent_data=test_params["send_data"].hex(),
539
+ received_data=None,
540
+ expected_data=test_params["expect_data"].hex() if test_params["expect_data"] else None,
541
+ latency=None,
542
+ timing_valid=None,
543
+ bit_errors=0,
544
+ )
545
+
546
+ def _evaluate_response(
547
+ self, response: bytes, expect_data: bytes | None
548
+ ) -> tuple[TestStatus, int]:
549
+ """Evaluate response against expected data."""
550
+ if expect_data is None:
551
+ return TestStatus.PASSED, 0
552
+ if response == expect_data:
553
+ return TestStatus.PASSED, 0
554
+ return TestStatus.FAILED, self._count_bit_errors(response, expect_data)
555
+
556
+ def _validate_timing(
557
+ self,
558
+ latency: float,
559
+ max_latency: float | None,
560
+ min_latency: float | None,
561
+ status: TestStatus,
562
+ ) -> bool | None:
563
+ """Validate timing constraints."""
564
+ if not self.config.validate_timing or (not max_latency and not min_latency):
565
+ return None
566
+ timing_valid = True
567
+ if max_latency and latency > max_latency:
568
+ timing_valid = False
569
+ if min_latency and latency < min_latency:
570
+ timing_valid = False
571
+ return timing_valid
572
+
573
+ def _create_error_result(self, test_params: dict[str, Any], error: Exception) -> HILTestResult:
574
+ """Create result for test with error."""
575
+ return HILTestResult(
576
+ test_name=test_params["test_name"],
577
+ status=TestStatus.ERROR,
578
+ sent_data=test_params["send_data"].hex(),
579
+ received_data=None,
580
+ expected_data=test_params["expect_data"].hex() if test_params["expect_data"] else None,
581
+ latency=None,
582
+ error=f"{type(error).__name__}: {error}",
583
+ )
584
+
585
+ def run_tests(self, test_cases: list[dict[str, Any]]) -> HILTestReport:
586
+ """Execute a suite of test cases.
587
+
588
+ Args:
589
+ test_cases: List of test case dictionaries (see run_test for format).
590
+
591
+ Returns:
592
+ Comprehensive test report with statistics and all results.
593
+
594
+ Example:
595
+ >>> config = HILConfig(interface="serial", port="/dev/ttyUSB0")
596
+ >>> tester = HILTester(config)
597
+ >>> tests = [
598
+ ... {"name": "test1", "send": b"\\x01", "expect": b"\\x02"},
599
+ ... {"name": "test2", "send": b"\\x03", "expect": b"\\x04"},
600
+ ... ]
601
+ >>> with tester:
602
+ ... report = tester.run_tests(tests)
603
+ >>> print(f"Success rate: {report.success_rate:.1%}")
604
+ """
605
+ start_time = time.time()
606
+ results: list[HILTestResult] = []
607
+ latencies: list[float] = []
608
+
609
+ for test_case in test_cases:
610
+ result = self.run_test(test_case)
611
+ results.append(result)
612
+ if result.latency is not None:
613
+ latencies.append(result.latency)
614
+
615
+ end_time = time.time()
616
+
617
+ # Calculate statistics
618
+ passed = sum(1 for r in results if r.status == TestStatus.PASSED)
619
+ failed = sum(1 for r in results if r.status == TestStatus.FAILED)
620
+ errors = sum(1 for r in results if r.status == TestStatus.ERROR)
621
+ timeouts = sum(1 for r in results if r.status == TestStatus.TIMEOUT)
622
+ skipped = sum(1 for r in results if r.status == TestStatus.SKIPPED)
623
+
624
+ timing_stats = {}
625
+ if latencies:
626
+ timing_stats = {
627
+ "min_latency": min(latencies),
628
+ "max_latency": max(latencies),
629
+ "avg_latency": sum(latencies) / len(latencies),
630
+ "total_samples": len(latencies),
631
+ }
632
+
633
+ # Interface is always InterfaceType after __post_init__
634
+ interface_value = (
635
+ self.config.interface.value
636
+ if isinstance(self.config.interface, InterfaceType)
637
+ else self.config.interface
638
+ )
639
+
640
+ hardware_info = {
641
+ "interface": interface_value,
642
+ "port": str(self.config.port),
643
+ "baud_rate": self.config.baud_rate,
644
+ "timeout": self.config.timeout,
645
+ "dry_run": self.config.dry_run,
646
+ }
647
+
648
+ return HILTestReport(
649
+ test_results=results,
650
+ total=len(results),
651
+ passed=passed,
652
+ failed=failed,
653
+ errors=errors,
654
+ timeouts=timeouts,
655
+ skipped=skipped,
656
+ hardware_info=hardware_info,
657
+ timing_statistics=timing_stats,
658
+ start_time=start_time,
659
+ end_time=end_time,
660
+ duration=end_time - start_time,
661
+ )
662
+
663
+ def _setup_gpio(self) -> None:
664
+ """Initialize GPIO controller.
665
+
666
+ Tries to use gpiod first (modern Linux), falls back to RPi.GPIO.
667
+
668
+ Raises:
669
+ ImportError: If no GPIO library is available.
670
+ """
671
+ if "gpiod" in globals() and gpiod is not None:
672
+ # Use libgpiod for modern Linux systems
673
+ self._gpio_controller = gpiod
674
+ elif GPIO is not None:
675
+ GPIO.setmode(GPIO.BCM)
676
+ GPIO.setwarnings(False)
677
+ self._gpio_controller = GPIO
678
+
679
+ # Setup pins as outputs
680
+ if self.config.reset_gpio is not None:
681
+ GPIO.setup(self.config.reset_gpio, GPIO.OUT, initial=GPIO.HIGH)
682
+ if self.config.power_gpio is not None:
683
+ GPIO.setup(self.config.power_gpio, GPIO.OUT, initial=GPIO.LOW)
684
+ else:
685
+ raise ImportError(
686
+ "No GPIO library available. Install gpiod or RPi.GPIO: "
687
+ "pip install gpiod # or pip install RPi.GPIO"
688
+ )
689
+
690
+ def _power_on(self) -> None:
691
+ """Power on device via GPIO."""
692
+ if self._gpio_controller is None or self.config.power_gpio is None:
693
+ return
694
+
695
+ # Assuming active-high power control
696
+ if hasattr(self._gpio_controller, "output"):
697
+ # RPi.GPIO
698
+ self._gpio_controller.output(self.config.power_gpio, True)
699
+ # Add gpiod support if needed
700
+
701
+ def _power_off(self) -> None:
702
+ """Power off device via GPIO."""
703
+ if self._gpio_controller is None or self.config.power_gpio is None:
704
+ return
705
+
706
+ if hasattr(self._gpio_controller, "output"):
707
+ # RPi.GPIO
708
+ self._gpio_controller.output(self.config.power_gpio, False)
709
+
710
+ def _reset_device(self) -> None:
711
+ """Reset device via GPIO pulse."""
712
+ if self._gpio_controller is None or self.config.reset_gpio is None:
713
+ return
714
+
715
+ # Assuming active-low reset (pulse low to reset)
716
+ if hasattr(self._gpio_controller, "output"):
717
+ # RPi.GPIO
718
+ self._gpio_controller.output(self.config.reset_gpio, False)
719
+ time.sleep(self.config.reset_duration)
720
+ self._gpio_controller.output(self.config.reset_gpio, True)
721
+
722
+ def _connect(self) -> None:
723
+ """Connect to hardware interface.
724
+
725
+ Raises:
726
+ ImportError: If required library is not installed.
727
+ OSError: If connection fails.
728
+ """
729
+ if self.config.interface == InterfaceType.SERIAL:
730
+ self._connect_serial()
731
+ elif self.config.interface == InterfaceType.SOCKETCAN:
732
+ self._connect_socketcan()
733
+ elif self.config.interface == InterfaceType.USB:
734
+ self._connect_usb()
735
+ elif self.config.interface == InterfaceType.SPI:
736
+ self._connect_spi()
737
+ elif self.config.interface == InterfaceType.I2C:
738
+ self._connect_i2c()
739
+
740
+ def _connect_serial(self) -> None:
741
+ """Connect to serial port.
742
+
743
+ Raises:
744
+ ImportError: If pyserial is not installed.
745
+ OSError: If serial port cannot be opened.
746
+ """
747
+ self._connection = connect_serial_port(
748
+ port=str(self.config.port),
749
+ baud_rate=self.config.baud_rate,
750
+ timeout=self.config.timeout,
751
+ )
752
+
753
+ def _connect_socketcan(self) -> None:
754
+ """Connect to SocketCAN interface.
755
+
756
+ Raises:
757
+ ImportError: If python-can is not installed.
758
+ OSError: If CAN interface cannot be opened.
759
+ """
760
+ if can is None:
761
+ raise ImportError(
762
+ "python-can is required for SocketCAN. Install with: pip install python-can"
763
+ )
764
+
765
+ if not isinstance(self.config.port, str):
766
+ raise ValueError(f"CAN interface must be string, got {type(self.config.port)}")
767
+
768
+ self._connection = can.interface.Bus(
769
+ channel=self.config.port, interface="socketcan", receive_own_messages=False
770
+ )
771
+
772
+ def _connect_usb(self) -> None:
773
+ """Connect to USB device.
774
+
775
+ Raises:
776
+ ImportError: If pyusb is not installed.
777
+ OSError: If USB device not found or cannot be opened.
778
+ """
779
+ if getattr(usb, "core", None) is None:
780
+ raise ImportError(
781
+ "pyusb is required for USB interface. Install with: pip install pyusb"
782
+ )
783
+
784
+ if self.config.usb_vendor_id is None or self.config.usb_product_id is None:
785
+ raise ValueError("usb_vendor_id and usb_product_id must be set for USB interface")
786
+
787
+ dev = usb.core.find(
788
+ idVendor=self.config.usb_vendor_id, idProduct=self.config.usb_product_id
789
+ )
790
+ if dev is None:
791
+ raise OSError(
792
+ f"USB device not found: {self.config.usb_vendor_id:04x}:"
793
+ f"{self.config.usb_product_id:04x}"
794
+ )
795
+
796
+ self._connection = dev
797
+
798
+ def _connect_spi(self) -> None:
799
+ """Connect to SPI device.
800
+
801
+ Raises:
802
+ ImportError: If spidev is not installed.
803
+ OSError: If SPI device cannot be opened.
804
+ """
805
+ if spidev is None:
806
+ raise ImportError(
807
+ "spidev is required for SPI interface. Install with: pip install spidev"
808
+ )
809
+
810
+ spi = spidev.SpiDev()
811
+ spi.open(self.config.spi_bus, self.config.spi_device)
812
+ spi.max_speed_hz = self.config.spi_speed_hz
813
+ self._connection = spi
814
+
815
+ def _connect_i2c(self) -> None:
816
+ """Connect to I2C device.
817
+
818
+ Raises:
819
+ ImportError: If smbus2 is not installed.
820
+ OSError: If I2C device cannot be opened.
821
+ """
822
+ if SMBus is None:
823
+ raise ImportError(
824
+ "smbus2 is required for I2C interface. Install with: pip install smbus2"
825
+ )
826
+
827
+ self._connection = SMBus(self.config.i2c_bus)
828
+
829
+ def _send_receive(self, data: bytes, timeout: float) -> bytes | None:
830
+ """Send data and receive response.
831
+
832
+ Args:
833
+ data: Data to send.
834
+ timeout: Timeout in seconds.
835
+
836
+ Returns:
837
+ Response data, or None if timeout.
838
+
839
+ Raises:
840
+ OSError: If send/receive fails.
841
+ """
842
+ if self.config.dry_run:
843
+ # In dry-run mode, echo the data back
844
+ time.sleep(0.001) # Simulate minimal latency
845
+ return data
846
+
847
+ if self.config.interface == InterfaceType.SERIAL:
848
+ return self._send_receive_serial(data, timeout)
849
+ elif self.config.interface == InterfaceType.SOCKETCAN:
850
+ return self._send_receive_socketcan(data, timeout)
851
+ elif self.config.interface == InterfaceType.USB:
852
+ return self._send_receive_usb(data, timeout)
853
+ elif self.config.interface == InterfaceType.SPI:
854
+ return self._send_receive_spi(data)
855
+ elif self.config.interface == InterfaceType.I2C:
856
+ return self._send_receive_i2c(data)
857
+
858
+ return None
859
+
860
+ def _send_receive_serial(self, data: bytes, timeout: float) -> bytes | None:
861
+ """Send/receive via serial port."""
862
+ ser: serial.Serial = self._connection
863
+ original_timeout = ser.timeout
864
+ ser.timeout = timeout
865
+ ser.reset_input_buffer()
866
+ ser.write(data)
867
+ ser.flush()
868
+
869
+ # Read response
870
+ response = ser.read(1024)
871
+ ser.timeout = original_timeout
872
+ return response if response else None
873
+
874
+ def _send_receive_socketcan(self, data: bytes, timeout: float) -> bytes | None:
875
+ """Send/receive via SocketCAN."""
876
+ bus: CANBusProtocol = self._connection
877
+ msg = can.Message(arbitration_id=0x123, data=data, is_extended_id=False)
878
+ bus.send(msg)
879
+
880
+ response_msg = bus.recv(timeout=timeout)
881
+ return bytes(response_msg.data) if response_msg else None
882
+
883
+ def _send_receive_usb(self, data: bytes, timeout: float) -> bytes | None:
884
+ """Send/receive via USB bulk transfer."""
885
+ dev = self._connection
886
+ endpoint_out = 0x01 # Typically endpoint 1 OUT
887
+ endpoint_in = 0x81 # Typically endpoint 1 IN
888
+
889
+ # Send data
890
+ dev.write(endpoint_out, data, int(timeout * 1000))
891
+
892
+ # Receive response
893
+ try:
894
+ response = dev.read(endpoint_in, 1024, int(timeout * 1000))
895
+ return bytes(response) if response else None
896
+ except Exception:
897
+ return None
898
+
899
+ def _send_receive_spi(self, data: bytes) -> bytes | None:
900
+ """Send/receive via SPI (full-duplex)."""
901
+ spi = self._connection
902
+ response = spi.xfer2(list(data))
903
+ return bytes(response)
904
+
905
+ def _send_receive_i2c(self, data: bytes) -> bytes | None:
906
+ """Send/receive via I2C."""
907
+ bus = self._connection
908
+ # Write data
909
+ for byte in data:
910
+ bus.write_byte(self.config.i2c_address, byte)
911
+
912
+ # Read response (assume same length as sent)
913
+ response = []
914
+ for _ in range(len(data)):
915
+ response.append(bus.read_byte(self.config.i2c_address))
916
+
917
+ return bytes(response)
918
+
919
+ def _count_bit_errors(self, received: bytes, expected: bytes) -> int:
920
+ """Count number of bit errors between received and expected data.
921
+
922
+ Args:
923
+ received: Received data.
924
+ expected: Expected data.
925
+
926
+ Returns:
927
+ Number of bit errors (Hamming distance).
928
+ """
929
+ # Pad shorter sequence
930
+ max_len = max(len(received), len(expected))
931
+ received_padded = received + b"\x00" * (max_len - len(received))
932
+ expected_padded = expected + b"\x00" * (max_len - len(expected))
933
+
934
+ bit_errors = 0
935
+ for r, e in zip(received_padded, expected_padded, strict=True):
936
+ # Count differing bits
937
+ xor = r ^ e
938
+ while xor:
939
+ bit_errors += xor & 1
940
+ xor >>= 1
941
+
942
+ return bit_errors
943
+
944
+ def _export_pcap(self) -> None:
945
+ """Export captured traffic to PCAP file.
946
+
947
+ Requires scapy to be installed.
948
+ """
949
+ if wrpcap is None or IP is None or UDP is None or Packet is None:
950
+ # Silently skip if scapy not available
951
+ return
952
+
953
+ packets: list[Any] = []
954
+ for timestamp, sent_data, recv_data in self._pcap_packets:
955
+ # Create UDP packet for sent data
956
+ pkt = IP(dst="192.168.1.1") / UDP(dport=12345) / bytes(sent_data)
957
+ pkt.time = timestamp
958
+ packets.append(pkt)
959
+
960
+ # Create UDP packet for received data if present
961
+ if recv_data:
962
+ pkt = IP(src="192.168.1.1") / UDP(sport=12345) / bytes(recv_data)
963
+ pkt.time = timestamp + 0.001 # Slight offset
964
+ packets.append(pkt)
965
+
966
+ if packets:
967
+ wrpcap(self.config.pcap_file, packets)
968
+
969
+
970
+ __all__ = [
971
+ "HILConfig",
972
+ "HILTestReport",
973
+ "HILTestResult",
974
+ "HILTester",
975
+ "InterfaceType",
976
+ "TestStatus",
977
+ ]