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,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
+ ]