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,809 @@
1
+ """BLE (Bluetooth Low Energy) protocol analyzer with GATT service discovery.
2
+
3
+ This module provides comprehensive BLE packet analysis including:
4
+ - Advertising packet parsing (ADV_IND, SCAN_RSP, etc.)
5
+ - ATT protocol operation decoding (Read, Write, Notify, etc.)
6
+ - GATT service/characteristic/descriptor discovery
7
+ - Standard and custom UUID mapping
8
+ - Export to JSON/CSV formats
9
+
10
+ Example:
11
+ >>> analyzer = BLEAnalyzer()
12
+ >>> packet = BLEPacket(
13
+ ... timestamp=0.0,
14
+ ... packet_type="ADV_IND",
15
+ ... source_address="AA:BB:CC:DD:EE:FF",
16
+ ... data=adv_data,
17
+ ... )
18
+ >>> analyzer.add_packet(packet)
19
+ >>> services = analyzer.discover_services()
20
+ >>> analyzer.export_services(Path("services.json"))
21
+
22
+ References:
23
+ Bluetooth Core Specification v5.4: https://www.bluetooth.com/specifications/specs/
24
+ GATT Specification Supplement: https://www.bluetooth.com/specifications/specs/
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import csv
30
+ import json
31
+ import struct
32
+ from dataclasses import dataclass, field
33
+ from pathlib import Path
34
+ from typing import Any
35
+
36
+ from oscura.analyzers.protocols.ble.uuids import (
37
+ AD_TYPES,
38
+ get_characteristic_name,
39
+ get_descriptor_name,
40
+ get_service_name,
41
+ uuid_to_string,
42
+ )
43
+
44
+ # BLE Link Layer packet types (PDU Type field in advertising channel)
45
+ BLE_PACKET_TYPES: dict[int, str] = {
46
+ 0x00: "ADV_IND", # Connectable undirected advertising
47
+ 0x01: "ADV_DIRECT_IND", # Connectable directed advertising
48
+ 0x02: "ADV_NONCONN_IND", # Non-connectable undirected advertising
49
+ 0x03: "SCAN_REQ", # Scan request
50
+ 0x04: "SCAN_RSP", # Scan response
51
+ 0x05: "CONNECT_REQ", # Connection request
52
+ 0x06: "ADV_SCAN_IND", # Scannable undirected advertising
53
+ }
54
+
55
+ # ATT Protocol Opcodes
56
+ ATT_OPCODES: dict[int, str] = {
57
+ 0x01: "Error Response",
58
+ 0x02: "Exchange MTU Request",
59
+ 0x03: "Exchange MTU Response",
60
+ 0x04: "Find Information Request",
61
+ 0x05: "Find Information Response",
62
+ 0x06: "Find By Type Value Request",
63
+ 0x07: "Find By Type Value Response",
64
+ 0x08: "Read By Type Request",
65
+ 0x09: "Read By Type Response",
66
+ 0x0A: "Read Request",
67
+ 0x0B: "Read Response",
68
+ 0x0C: "Read Blob Request",
69
+ 0x0D: "Read Blob Response",
70
+ 0x0E: "Read Multiple Request",
71
+ 0x0F: "Read Multiple Response",
72
+ 0x10: "Read By Group Type Request",
73
+ 0x11: "Read By Group Type Response",
74
+ 0x12: "Write Request",
75
+ 0x13: "Write Response",
76
+ 0x16: "Prepare Write Request",
77
+ 0x17: "Prepare Write Response",
78
+ 0x18: "Execute Write Request",
79
+ 0x19: "Execute Write Response",
80
+ 0x1B: "Handle Value Notification",
81
+ 0x1D: "Handle Value Indication",
82
+ 0x1E: "Handle Value Confirmation",
83
+ 0x52: "Write Command",
84
+ 0xD2: "Signed Write Command",
85
+ }
86
+
87
+ # GATT Characteristic properties (bit mask)
88
+ GATT_CHAR_PROPERTIES: dict[int, str] = {
89
+ 0x01: "broadcast",
90
+ 0x02: "read",
91
+ 0x04: "write_no_response",
92
+ 0x08: "write",
93
+ 0x10: "notify",
94
+ 0x20: "indicate",
95
+ 0x40: "authenticated_signed_writes",
96
+ 0x80: "extended_properties",
97
+ }
98
+
99
+
100
+ @dataclass
101
+ class GATTDescriptor:
102
+ """GATT descriptor definition.
103
+
104
+ Attributes:
105
+ uuid: Descriptor UUID (16-bit or 128-bit).
106
+ name: Human-readable name.
107
+ handle: Attribute handle.
108
+ value: Descriptor value (optional).
109
+ """
110
+
111
+ uuid: str
112
+ name: str
113
+ handle: int
114
+ value: bytes | None = None
115
+
116
+ def to_dict(self) -> dict[str, Any]:
117
+ """Convert to dictionary for serialization.
118
+
119
+ Returns:
120
+ Dictionary representation.
121
+ """
122
+ return {
123
+ "uuid": self.uuid,
124
+ "name": self.name,
125
+ "handle": self.handle,
126
+ "value": self.value.hex() if self.value else None,
127
+ }
128
+
129
+
130
+ @dataclass
131
+ class GATTCharacteristic:
132
+ """GATT characteristic definition.
133
+
134
+ Attributes:
135
+ uuid: Characteristic UUID.
136
+ name: Human-readable name.
137
+ properties: List of properties (read, write, notify, etc.).
138
+ handle: Attribute handle.
139
+ value_handle: Value handle (for read/write operations).
140
+ value: Characteristic value (optional).
141
+ descriptors: List of descriptors.
142
+ """
143
+
144
+ uuid: str
145
+ name: str
146
+ properties: list[str]
147
+ handle: int
148
+ value_handle: int | None = None
149
+ value: bytes | None = None
150
+ descriptors: list[GATTDescriptor] = field(default_factory=list)
151
+
152
+ def to_dict(self) -> dict[str, Any]:
153
+ """Convert to dictionary for serialization.
154
+
155
+ Returns:
156
+ Dictionary representation.
157
+ """
158
+ return {
159
+ "uuid": self.uuid,
160
+ "name": self.name,
161
+ "properties": self.properties,
162
+ "handle": self.handle,
163
+ "value_handle": self.value_handle,
164
+ "value": self.value.hex() if self.value else None,
165
+ "descriptors": [d.to_dict() for d in self.descriptors],
166
+ }
167
+
168
+
169
+ @dataclass
170
+ class GATTService:
171
+ """GATT service definition.
172
+
173
+ Attributes:
174
+ uuid: Service UUID.
175
+ name: Human-readable name.
176
+ characteristics: List of characteristics.
177
+ handle_range: (start_handle, end_handle) range.
178
+ """
179
+
180
+ uuid: str
181
+ name: str
182
+ characteristics: list[GATTCharacteristic]
183
+ handle_range: tuple[int, int]
184
+
185
+ def to_dict(self) -> dict[str, Any]:
186
+ """Convert to dictionary for serialization.
187
+
188
+ Returns:
189
+ Dictionary representation.
190
+ """
191
+ return {
192
+ "uuid": self.uuid,
193
+ "name": self.name,
194
+ "handle_range": list(self.handle_range),
195
+ "characteristics": [c.to_dict() for c in self.characteristics],
196
+ }
197
+
198
+
199
+ @dataclass
200
+ class BLEPacket:
201
+ """Represents a BLE packet.
202
+
203
+ Attributes:
204
+ timestamp: Packet timestamp in seconds.
205
+ packet_type: Packet type (e.g., "ADV_IND", "ATT_READ_REQ").
206
+ source_address: Source MAC address (AA:BB:CC:DD:EE:FF).
207
+ dest_address: Destination MAC address (optional).
208
+ rssi: Received Signal Strength Indicator in dBm (optional).
209
+ data: Raw packet data.
210
+ decoded: Decoded packet contents (optional).
211
+ """
212
+
213
+ timestamp: float
214
+ packet_type: str
215
+ source_address: str
216
+ data: bytes
217
+ dest_address: str | None = None
218
+ rssi: int | None = None
219
+ decoded: dict[str, Any] | None = None
220
+
221
+ def to_dict(self) -> dict[str, Any]:
222
+ """Convert to dictionary for serialization.
223
+
224
+ Returns:
225
+ Dictionary representation.
226
+ """
227
+ return {
228
+ "timestamp": self.timestamp,
229
+ "packet_type": self.packet_type,
230
+ "source_address": self.source_address,
231
+ "dest_address": self.dest_address,
232
+ "rssi": self.rssi,
233
+ "data": self.data.hex(),
234
+ "decoded": self.decoded,
235
+ }
236
+
237
+
238
+ def _decode_error_response(data: bytes, result: dict[str, Any]) -> None:
239
+ """Decode ATT Error Response packet.
240
+
241
+ Args:
242
+ data: ATT packet data.
243
+ result: Result dictionary to populate.
244
+ """
245
+ if len(data) >= 5:
246
+ result["request_opcode"] = f"0x{data[1]:02X}"
247
+ result["handle"] = int.from_bytes(data[2:4], "little")
248
+ result["error_code"] = f"0x{data[4]:02X}"
249
+
250
+
251
+ def _decode_mtu_operation(data: bytes, result: dict[str, Any]) -> None:
252
+ """Decode ATT MTU Request/Response.
253
+
254
+ Args:
255
+ data: ATT packet data.
256
+ result: Result dictionary to populate.
257
+ """
258
+ if len(data) >= 3:
259
+ result["mtu"] = int.from_bytes(data[1:3], "little")
260
+
261
+
262
+ def _decode_read_request(data: bytes, result: dict[str, Any]) -> None:
263
+ """Decode ATT Read Request/Blob Request.
264
+
265
+ Args:
266
+ data: ATT packet data.
267
+ result: Result dictionary to populate.
268
+ """
269
+ if len(data) >= 3:
270
+ result["handle"] = int.from_bytes(data[1:3], "little")
271
+
272
+
273
+ def _decode_read_response(data: bytes, result: dict[str, Any]) -> None:
274
+ """Decode ATT Read Response.
275
+
276
+ Args:
277
+ data: ATT packet data.
278
+ result: Result dictionary to populate.
279
+ """
280
+ result["value"] = data[1:].hex()
281
+
282
+
283
+ def _decode_read_by_type_request(data: bytes, result: dict[str, Any]) -> None:
284
+ """Decode ATT Read By Type/Group Type Request.
285
+
286
+ Args:
287
+ data: ATT packet data.
288
+ result: Result dictionary to populate.
289
+ """
290
+ if len(data) >= 7:
291
+ result["start_handle"] = int.from_bytes(data[1:3], "little")
292
+ result["end_handle"] = int.from_bytes(data[3:5], "little")
293
+ uuid_data = data[5:]
294
+ result["uuid"] = uuid_to_string(uuid_data)
295
+
296
+
297
+ def _decode_read_by_type_response(data: bytes, result: dict[str, Any]) -> None:
298
+ """Decode ATT Read By Type/Group Type Response.
299
+
300
+ Args:
301
+ data: ATT packet data.
302
+ result: Result dictionary to populate.
303
+ """
304
+ if len(data) >= 2:
305
+ length = data[1]
306
+ result["attribute_length"] = length
307
+
308
+ attributes = []
309
+ i = 2
310
+ while i + length <= len(data):
311
+ attr_data = data[i : i + length]
312
+ if len(attr_data) >= 2:
313
+ handle = int.from_bytes(attr_data[0:2], "little")
314
+ attributes.append({"handle": handle, "data": attr_data[2:].hex()})
315
+ i += length
316
+ result["attributes"] = attributes
317
+
318
+
319
+ def _decode_write_operation(data: bytes, result: dict[str, Any]) -> None:
320
+ """Decode ATT Write operations (Request/Notification/Indication/Command).
321
+
322
+ Args:
323
+ data: ATT packet data.
324
+ result: Result dictionary to populate.
325
+ """
326
+ if len(data) >= 3:
327
+ result["handle"] = int.from_bytes(data[1:3], "little")
328
+ result["value"] = data[3:].hex()
329
+
330
+
331
+ class BLEAnalyzer:
332
+ """BLE protocol analyzer with GATT service discovery.
333
+
334
+ This analyzer processes BLE packets to extract advertising data,
335
+ decode ATT operations, and discover GATT services/characteristics.
336
+
337
+ Example:
338
+ >>> analyzer = BLEAnalyzer()
339
+ >>> analyzer.register_custom_uuid("0xABCD", "My Custom Service")
340
+ >>> analyzer.add_packet(packet)
341
+ >>> services = analyzer.discover_services()
342
+ >>> print(f"Found {len(services)} services")
343
+ """
344
+
345
+ def __init__(self) -> None:
346
+ """Initialize BLE analyzer."""
347
+ self.packets: list[BLEPacket] = []
348
+ self.services: list[GATTService] = []
349
+ self.custom_uuids: dict[str, str] = {}
350
+ self._service_cache: dict[int, GATTService] = {}
351
+ self._char_cache: dict[int, GATTCharacteristic] = {}
352
+
353
+ def add_packet(self, packet: BLEPacket) -> None:
354
+ """Add BLE packet for analysis.
355
+
356
+ Args:
357
+ packet: BLE packet to add.
358
+
359
+ Example:
360
+ >>> packet = BLEPacket(
361
+ ... timestamp=0.0,
362
+ ... packet_type="ADV_IND",
363
+ ... source_address="AA:BB:CC:DD:EE:FF",
364
+ ... data=b"\\x02\\x01\\x06\\x09\\x09MyDevice",
365
+ ... )
366
+ >>> analyzer.add_packet(packet)
367
+ """
368
+ # Decode packet based on type
369
+ if packet.packet_type.startswith("ADV_") or packet.packet_type == "SCAN_RSP":
370
+ packet.decoded = self.parse_advertising_data(packet.data)
371
+ elif packet.packet_type.startswith("ATT_"):
372
+ packet.decoded = self.decode_att_operation(packet.data)
373
+
374
+ self.packets.append(packet)
375
+
376
+ def parse_advertising_data(self, data: bytes) -> dict[str, Any]:
377
+ """Parse BLE advertising data (AD structures).
378
+
379
+ Args:
380
+ data: Advertising data payload.
381
+
382
+ Returns:
383
+ Dictionary of parsed AD structures.
384
+
385
+ Example:
386
+ >>> data = b"\\x02\\x01\\x06\\x09\\x09MyDevice"
387
+ >>> result = analyzer.parse_advertising_data(data)
388
+ >>> print(result["name"])
389
+ 'MyDevice'
390
+ """
391
+ result: dict[str, Any] = {}
392
+ i = 0
393
+
394
+ while i < len(data):
395
+ if i >= len(data):
396
+ break
397
+
398
+ length = data[i]
399
+ if length == 0 or i + length >= len(data):
400
+ break
401
+
402
+ ad_type = data[i + 1]
403
+ ad_data = data[i + 2 : i + 1 + length]
404
+
405
+ # Parse AD structure by type
406
+ self._parse_ad_structure(ad_type, ad_data, result)
407
+
408
+ i += 1 + length
409
+
410
+ return result
411
+
412
+ def _parse_ad_structure(self, ad_type: int, ad_data: bytes, result: dict[str, Any]) -> None:
413
+ """Parse single AD structure into result dictionary.
414
+
415
+ Args:
416
+ ad_type: AD type code.
417
+ ad_data: AD data bytes.
418
+ result: Result dictionary to update.
419
+ """
420
+ if ad_type == 0x01:
421
+ self._parse_flags(ad_data, result)
422
+ elif ad_type in [0x08, 0x09]:
423
+ result["name"] = ad_data.decode("utf-8", errors="ignore")
424
+ elif ad_type == 0x0A:
425
+ result["tx_power"] = struct.unpack("b", ad_data)[0]
426
+ elif ad_type in [0x02, 0x03]:
427
+ self._parse_service_uuids(ad_data, result)
428
+ elif ad_type == 0x16:
429
+ self._parse_service_data(ad_data, result)
430
+ elif ad_type == 0x19:
431
+ self._parse_appearance(ad_data, result)
432
+ elif ad_type == 0xFF:
433
+ self._parse_manufacturer_data(ad_data, result)
434
+ else:
435
+ ad_type_name = AD_TYPES.get(ad_type, f"Unknown Type 0x{ad_type:02X}")
436
+ result[ad_type_name] = ad_data.hex()
437
+
438
+ def _parse_flags(self, ad_data: bytes, result: dict[str, Any]) -> None:
439
+ """Parse BLE flags AD structure.
440
+
441
+ Args:
442
+ ad_data: Flags data bytes.
443
+ result: Result dictionary to update.
444
+ """
445
+ flags = int.from_bytes(ad_data, "little")
446
+ result["flags"] = {
447
+ "value": flags,
448
+ "le_limited_discoverable": bool(flags & 0x01),
449
+ "le_general_discoverable": bool(flags & 0x02),
450
+ "br_edr_not_supported": bool(flags & 0x04),
451
+ "le_br_edr_controller": bool(flags & 0x08),
452
+ "le_br_edr_host": bool(flags & 0x10),
453
+ }
454
+
455
+ def _parse_service_uuids(self, ad_data: bytes, result: dict[str, Any]) -> None:
456
+ """Parse 16-bit service UUIDs AD structure.
457
+
458
+ Args:
459
+ ad_data: UUID data bytes.
460
+ result: Result dictionary to update.
461
+ """
462
+ uuids = []
463
+ for j in range(0, len(ad_data), 2):
464
+ uuid_val = int.from_bytes(ad_data[j : j + 2], "little")
465
+ uuids.append(f"0x{uuid_val:04X}")
466
+ result["service_uuids"] = uuids
467
+
468
+ def _parse_service_data(self, ad_data: bytes, result: dict[str, Any]) -> None:
469
+ """Parse service data AD structure.
470
+
471
+ Args:
472
+ ad_data: Service data bytes.
473
+ result: Result dictionary to update.
474
+ """
475
+ if len(ad_data) >= 2:
476
+ uuid_val = int.from_bytes(ad_data[0:2], "little")
477
+ service_uuid = f"0x{uuid_val:04X}"
478
+ result["service_data"] = {
479
+ "uuid": service_uuid,
480
+ "data": ad_data[2:].hex(),
481
+ }
482
+
483
+ def _parse_appearance(self, ad_data: bytes, result: dict[str, Any]) -> None:
484
+ """Parse appearance AD structure.
485
+
486
+ Args:
487
+ ad_data: Appearance data bytes.
488
+ result: Result dictionary to update.
489
+ """
490
+ if len(ad_data) >= 2:
491
+ appearance = int.from_bytes(ad_data, "little")
492
+ result["appearance"] = appearance
493
+
494
+ def _parse_manufacturer_data(self, ad_data: bytes, result: dict[str, Any]) -> None:
495
+ """Parse manufacturer data AD structure.
496
+
497
+ Args:
498
+ ad_data: Manufacturer data bytes.
499
+ result: Result dictionary to update.
500
+ """
501
+ if len(ad_data) >= 2:
502
+ company_id = int.from_bytes(ad_data[0:2], "little")
503
+ result["manufacturer_data"] = {
504
+ "company_id": f"0x{company_id:04X}",
505
+ "data": ad_data[2:].hex(),
506
+ }
507
+
508
+ def decode_att_operation(self, data: bytes) -> dict[str, Any]:
509
+ """Decode ATT protocol operation.
510
+
511
+ Args:
512
+ data: ATT packet payload.
513
+
514
+ Returns:
515
+ Dictionary of decoded operation details.
516
+
517
+ Example:
518
+ >>> data = b"\\x0A\\x03\\x00" # Read Request, handle 0x0003
519
+ >>> result = analyzer.decode_att_operation(data)
520
+ >>> print(result["opcode_name"])
521
+ 'Read Request'
522
+ """
523
+ if len(data) < 1:
524
+ return {"error": "Packet too short"}
525
+
526
+ opcode = data[0]
527
+ opcode_name = ATT_OPCODES.get(opcode, f"Unknown Opcode 0x{opcode:02X}")
528
+ result: dict[str, Any] = {
529
+ "opcode": f"0x{opcode:02X}",
530
+ "opcode_name": opcode_name,
531
+ }
532
+
533
+ try:
534
+ # Decode based on opcode category
535
+ if opcode == 0x01:
536
+ _decode_error_response(data, result)
537
+ elif opcode in [0x02, 0x03]:
538
+ _decode_mtu_operation(data, result)
539
+ elif opcode in [0x0A, 0x0C]:
540
+ _decode_read_request(data, result)
541
+ elif opcode == 0x0B:
542
+ _decode_read_response(data, result)
543
+ elif opcode in [0x08, 0x10]:
544
+ _decode_read_by_type_request(data, result)
545
+ elif opcode in [0x09, 0x11]:
546
+ _decode_read_by_type_response(data, result)
547
+ elif opcode in [0x12, 0x1B, 0x1D, 0x52]:
548
+ _decode_write_operation(data, result)
549
+
550
+ except (struct.error, IndexError) as e:
551
+ result["parse_error"] = str(e)
552
+
553
+ return result
554
+
555
+ def discover_services(self) -> list[GATTService]:
556
+ """Discover GATT services from captured ATT packets.
557
+
558
+ Analyzes Read By Group Type responses to build service hierarchy.
559
+
560
+ Returns:
561
+ List of discovered GATT services.
562
+
563
+ Example:
564
+ >>> services = analyzer.discover_services()
565
+ >>> for service in services:
566
+ ... print(f"{service.name}: {service.uuid}")
567
+ """
568
+ self.services.clear()
569
+ self._service_cache.clear()
570
+ self._char_cache.clear()
571
+
572
+ # Find service discovery responses (Read By Group Type Response, UUID 0x2800)
573
+ for packet in self.packets:
574
+ if not packet.decoded:
575
+ continue
576
+
577
+ opcode_name = packet.decoded.get("opcode_name", "")
578
+
579
+ # Service discovery (Primary Service = 0x2800)
580
+ if opcode_name == "Read By Group Type Response":
581
+ attributes = packet.decoded.get("attributes", [])
582
+ for attr in attributes:
583
+ try:
584
+ handle = attr["handle"]
585
+ data_hex = attr["data"]
586
+ data = bytes.fromhex(data_hex)
587
+
588
+ if len(data) >= 4:
589
+ # Format: end_handle (2) + UUID (2 or 16)
590
+ end_handle = int.from_bytes(data[0:2], "little")
591
+ uuid_data = data[2:]
592
+ uuid = uuid_to_string(uuid_data)
593
+
594
+ # Get service name
595
+ service_name = self.get_uuid_name(uuid, "service")
596
+
597
+ service = GATTService(
598
+ uuid=uuid,
599
+ name=service_name,
600
+ characteristics=[],
601
+ handle_range=(handle, end_handle),
602
+ )
603
+ self.services.append(service)
604
+ self._service_cache[handle] = service
605
+ except (ValueError, KeyError):
606
+ continue
607
+
608
+ # Characteristic discovery (Characteristic Declaration = 0x2803)
609
+ elif opcode_name == "Read By Type Response":
610
+ attributes = packet.decoded.get("attributes", [])
611
+ for attr in attributes:
612
+ try:
613
+ handle = attr["handle"]
614
+ data_hex = attr["data"]
615
+ data = bytes.fromhex(data_hex)
616
+
617
+ if len(data) >= 5:
618
+ # Format: properties (1) + value_handle (2) + UUID (2 or 16)
619
+ properties_byte = data[0]
620
+ value_handle = int.from_bytes(data[1:3], "little")
621
+ uuid_data = data[3:]
622
+ uuid = uuid_to_string(uuid_data)
623
+
624
+ # Parse properties
625
+ properties = self._parse_properties(properties_byte)
626
+
627
+ # Get characteristic name
628
+ char_name = self.get_uuid_name(uuid, "characteristic")
629
+
630
+ char = GATTCharacteristic(
631
+ uuid=uuid,
632
+ name=char_name,
633
+ properties=properties,
634
+ handle=handle,
635
+ value_handle=value_handle,
636
+ )
637
+
638
+ # Find parent service
639
+ for service in self.services:
640
+ if service.handle_range[0] <= handle <= service.handle_range[1]:
641
+ service.characteristics.append(char)
642
+ self._char_cache[handle] = char
643
+ break
644
+ except (ValueError, KeyError):
645
+ continue
646
+
647
+ return self.services
648
+
649
+ def _parse_properties(self, properties_byte: int) -> list[str]:
650
+ """Parse GATT characteristic properties byte.
651
+
652
+ Args:
653
+ properties_byte: Properties bit mask.
654
+
655
+ Returns:
656
+ List of property names.
657
+ """
658
+ properties = []
659
+ for bit, name in GATT_CHAR_PROPERTIES.items():
660
+ if properties_byte & bit:
661
+ properties.append(name)
662
+ return properties
663
+
664
+ def register_custom_uuid(self, uuid: str, name: str) -> None:
665
+ """Register custom service/characteristic UUID.
666
+
667
+ Args:
668
+ uuid: UUID string (e.g., "0xABCD" or full format).
669
+ name: Human-readable name.
670
+
671
+ Example:
672
+ >>> analyzer.register_custom_uuid("0xABCD", "My Custom Service")
673
+ """
674
+ self.custom_uuids[uuid.upper()] = name
675
+
676
+ def get_uuid_name(self, uuid: str, uuid_type: str = "service") -> str:
677
+ """Get name for UUID (checks custom mappings first).
678
+
679
+ Args:
680
+ uuid: UUID string.
681
+ uuid_type: Type of UUID ("service", "characteristic", "descriptor").
682
+
683
+ Returns:
684
+ Human-readable name.
685
+ """
686
+ # Check custom mappings first
687
+ if uuid.upper() in self.custom_uuids:
688
+ return self.custom_uuids[uuid.upper()]
689
+
690
+ # Check standard mappings
691
+ if uuid_type == "service":
692
+ return get_service_name(uuid)
693
+ elif uuid_type == "characteristic":
694
+ return get_characteristic_name(uuid)
695
+ elif uuid_type == "descriptor":
696
+ return get_descriptor_name(uuid)
697
+ else:
698
+ return f"Unknown {uuid_type}"
699
+
700
+ def export_services(self, output_path: Path, format: str = "json") -> None:
701
+ """Export discovered services to file.
702
+
703
+ Args:
704
+ output_path: Output file path.
705
+ format: Export format ("json" or "csv").
706
+
707
+ Raises:
708
+ ValueError: If format is not supported.
709
+
710
+ Example:
711
+ >>> analyzer.export_services(Path("services.json"))
712
+ >>> analyzer.export_services(Path("services.csv"), format="csv")
713
+ """
714
+ if format == "json":
715
+ self._export_json(output_path)
716
+ elif format == "csv":
717
+ self._export_csv(output_path)
718
+ else:
719
+ raise ValueError(f"Unsupported format: {format}")
720
+
721
+ def _export_json(self, output_path: Path) -> None:
722
+ """Export services as JSON.
723
+
724
+ Args:
725
+ output_path: Output file path.
726
+ """
727
+ data = {
728
+ "services": [service.to_dict() for service in self.services],
729
+ "packet_count": len(self.packets),
730
+ }
731
+ with output_path.open("w") as f:
732
+ json.dump(data, f, indent=2)
733
+
734
+ def _export_csv(self, output_path: Path) -> None:
735
+ """Export services as CSV.
736
+
737
+ Args:
738
+ output_path: Output file path.
739
+ """
740
+ with output_path.open("w", newline="") as f:
741
+ writer = csv.writer(f)
742
+ writer.writerow(
743
+ [
744
+ "Service UUID",
745
+ "Service Name",
746
+ "Handle Range",
747
+ "Characteristic UUID",
748
+ "Characteristic Name",
749
+ "Properties",
750
+ ]
751
+ )
752
+
753
+ for service in self.services:
754
+ if not service.characteristics:
755
+ writer.writerow(
756
+ [
757
+ service.uuid,
758
+ service.name,
759
+ f"{service.handle_range[0]}-{service.handle_range[1]}",
760
+ "",
761
+ "",
762
+ "",
763
+ ]
764
+ )
765
+ else:
766
+ for char in service.characteristics:
767
+ writer.writerow(
768
+ [
769
+ service.uuid,
770
+ service.name,
771
+ f"{service.handle_range[0]}-{service.handle_range[1]}",
772
+ char.uuid,
773
+ char.name,
774
+ ", ".join(char.properties),
775
+ ]
776
+ )
777
+
778
+ def get_statistics(self) -> dict[str, Any]:
779
+ """Get analysis statistics.
780
+
781
+ Returns:
782
+ Dictionary of statistics.
783
+
784
+ Example:
785
+ >>> stats = analyzer.get_statistics()
786
+ >>> print(f"Total packets: {stats['total_packets']}")
787
+ """
788
+ packet_types: dict[str, int] = {}
789
+ for packet in self.packets:
790
+ packet_types[packet.packet_type] = packet_types.get(packet.packet_type, 0) + 1
791
+
792
+ return {
793
+ "total_packets": len(self.packets),
794
+ "packet_types": packet_types,
795
+ "services_discovered": len(self.services),
796
+ "total_characteristics": sum(len(s.characteristics) for s in self.services),
797
+ }
798
+
799
+
800
+ __all__ = [
801
+ "ATT_OPCODES",
802
+ "BLE_PACKET_TYPES",
803
+ "GATT_CHAR_PROPERTIES",
804
+ "BLEAnalyzer",
805
+ "BLEPacket",
806
+ "GATTCharacteristic",
807
+ "GATTDescriptor",
808
+ "GATTService",
809
+ ]