oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (497) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,153 @@
1
+ """Zigbee security and encryption support.
2
+
3
+ This module provides utilities for handling Zigbee security frames
4
+ including network key management and frame decryption support.
5
+
6
+ Note: This module provides structure parsing only. Actual AES-CCM
7
+ decryption requires cryptography libraries and valid network keys.
8
+
9
+ References:
10
+ Zigbee Specification Section 4.5 (Security)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any
16
+
17
+
18
+ def parse_security_header(data: bytes) -> dict[str, Any]:
19
+ """Parse Zigbee security header from NWK auxiliary header.
20
+
21
+ Args:
22
+ data: Security header bytes.
23
+
24
+ Returns:
25
+ Parsed security header fields.
26
+
27
+ Example:
28
+ >>> header = parse_security_header(security_data)
29
+ >>> print(header['security_level'])
30
+ 5
31
+ """
32
+ if len(data) < 5:
33
+ return {"error": "Insufficient data for security header"}
34
+
35
+ security_control = data[0]
36
+ frame_counter = int.from_bytes(data[1:5], "little")
37
+
38
+ security_level = security_control & 0x07
39
+ key_identifier = (security_control >> 3) & 0x03
40
+ extended_nonce = bool(security_control & 0x20)
41
+
42
+ result: dict[str, Any] = {
43
+ "security_control": security_control,
44
+ "security_level": security_level,
45
+ "key_identifier": key_identifier,
46
+ "extended_nonce": extended_nonce,
47
+ "frame_counter": frame_counter,
48
+ }
49
+
50
+ offset = 5
51
+
52
+ # Extended nonce includes source address
53
+ if extended_nonce:
54
+ if len(data) < offset + 8:
55
+ result["error"] = "Insufficient data for extended nonce"
56
+ return result
57
+ source_address = int.from_bytes(data[offset : offset + 8], "little")
58
+ result["source_address"] = source_address
59
+ offset += 8
60
+
61
+ # Key sequence number (if key identifier indicates it)
62
+ if key_identifier == 0x01: # Network key
63
+ if len(data) < offset + 1:
64
+ result["error"] = "Insufficient data for key sequence"
65
+ return result
66
+ result["key_sequence_number"] = data[offset]
67
+ offset += 1
68
+
69
+ result["header_length"] = offset
70
+ return result
71
+
72
+
73
+ def is_frame_encrypted(nwk_frame_control: int) -> bool:
74
+ """Check if NWK frame is encrypted.
75
+
76
+ Args:
77
+ nwk_frame_control: NWK frame control field (2 bytes as int).
78
+
79
+ Returns:
80
+ True if frame is encrypted, False otherwise.
81
+
82
+ Example:
83
+ >>> frame_control = 0x0208 # Security enabled
84
+ >>> is_frame_encrypted(frame_control)
85
+ True
86
+ """
87
+ # Security bit is bit 1 of the high byte (bit 9 overall)
88
+ return bool((nwk_frame_control >> 9) & 0x01)
89
+
90
+
91
+ def get_security_level_name(level: int) -> str:
92
+ """Get human-readable security level name.
93
+
94
+ Args:
95
+ level: Security level (0-7).
96
+
97
+ Returns:
98
+ Security level name.
99
+
100
+ Example:
101
+ >>> get_security_level_name(5)
102
+ 'ENC-MIC-32'
103
+ """
104
+ levels = {
105
+ 0x00: "None",
106
+ 0x01: "MIC-32",
107
+ 0x02: "MIC-64",
108
+ 0x03: "MIC-128",
109
+ 0x04: "ENC",
110
+ 0x05: "ENC-MIC-32",
111
+ 0x06: "ENC-MIC-64",
112
+ 0x07: "ENC-MIC-128",
113
+ }
114
+ return levels.get(level, f"Unknown ({level})")
115
+
116
+
117
+ def decrypt_frame(
118
+ encrypted_payload: bytes,
119
+ network_key: bytes,
120
+ nonce: bytes,
121
+ security_level: int,
122
+ ) -> bytes | None:
123
+ """Decrypt Zigbee encrypted frame.
124
+
125
+ Note: This is a placeholder implementation. Actual decryption
126
+ requires AES-CCM implementation with proper nonce construction.
127
+
128
+ Args:
129
+ encrypted_payload: Encrypted payload with MIC.
130
+ network_key: 128-bit AES network key.
131
+ nonce: 13-byte nonce.
132
+ security_level: Security level (determines MIC size).
133
+
134
+ Returns:
135
+ Decrypted payload or None if decryption not available.
136
+
137
+ Example:
138
+ >>> # Requires cryptography library
139
+ >>> decrypted = decrypt_frame(payload, key, nonce, 5)
140
+ """
141
+ # Placeholder - real implementation would use AES-CCM
142
+ # from cryptography.hazmat.primitives.ciphers.aead import AESCCM
143
+ # cipher = AESCCM(network_key)
144
+ # return cipher.decrypt(nonce, encrypted_payload, None)
145
+ return None # Not implemented without cryptography dependencies
146
+
147
+
148
+ __all__ = [
149
+ "decrypt_frame",
150
+ "get_security_level_name",
151
+ "is_frame_encrypted",
152
+ "parse_security_header",
153
+ ]
@@ -0,0 +1,349 @@
1
+ """Zigbee Cluster Library (ZCL) definitions and parsers.
2
+
3
+ This module contains standard ZCL cluster definitions and frame parsers
4
+ for common clusters including On/Off, Level Control, Temperature, etc.
5
+
6
+ References:
7
+ Zigbee Cluster Library Specification (CSA-IOT)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ # Standard ZCL cluster IDs (subset of most common clusters)
15
+ ZCL_CLUSTERS: dict[int, str] = {
16
+ 0x0000: "Basic",
17
+ 0x0001: "Power Configuration",
18
+ 0x0003: "Identify",
19
+ 0x0004: "Groups",
20
+ 0x0005: "Scenes",
21
+ 0x0006: "On/Off",
22
+ 0x0008: "Level Control",
23
+ 0x0009: "Alarms",
24
+ 0x000A: "Time",
25
+ 0x0020: "Poll Control",
26
+ 0x0201: "Thermostat",
27
+ 0x0300: "Color Control",
28
+ 0x0400: "Illuminance Measurement",
29
+ 0x0402: "Temperature Measurement",
30
+ 0x0403: "Pressure Measurement",
31
+ 0x0404: "Flow Measurement",
32
+ 0x0405: "Relative Humidity Measurement",
33
+ 0x0406: "Occupancy Sensing",
34
+ 0x0500: "IAS Zone",
35
+ 0x0702: "Metering",
36
+ 0x0B04: "Electrical Measurement",
37
+ }
38
+
39
+ # ZCL frame control field bits
40
+ ZCL_FRAME_TYPE_MASK = 0x03
41
+ ZCL_FRAME_TYPE_GLOBAL = 0x00
42
+ ZCL_FRAME_TYPE_CLUSTER = 0x01
43
+
44
+ ZCL_MANUFACTURER_SPECIFIC = 0x04
45
+ ZCL_DIRECTION_SERVER_TO_CLIENT = 0x08
46
+ ZCL_DISABLE_DEFAULT_RESPONSE = 0x10
47
+
48
+ # Global ZCL commands
49
+ ZCL_GLOBAL_COMMANDS = {
50
+ 0x00: "Read Attributes",
51
+ 0x01: "Read Attributes Response",
52
+ 0x02: "Write Attributes",
53
+ 0x03: "Write Attributes Undivided",
54
+ 0x04: "Write Attributes Response",
55
+ 0x05: "Write Attributes No Response",
56
+ 0x06: "Configure Reporting",
57
+ 0x07: "Configure Reporting Response",
58
+ 0x08: "Read Reporting Configuration",
59
+ 0x09: "Read Reporting Configuration Response",
60
+ 0x0A: "Report Attributes",
61
+ 0x0B: "Default Response",
62
+ 0x0C: "Discover Attributes",
63
+ 0x0D: "Discover Attributes Response",
64
+ 0x0E: "Read Attributes Structured",
65
+ 0x0F: "Write Attributes Structured",
66
+ 0x10: "Write Attributes Structured Response",
67
+ 0x11: "Discover Commands Received",
68
+ 0x12: "Discover Commands Received Response",
69
+ 0x13: "Discover Commands Generated",
70
+ 0x14: "Discover Commands Generated Response",
71
+ 0x15: "Discover Attributes Extended",
72
+ 0x16: "Discover Attributes Extended Response",
73
+ }
74
+
75
+ # On/Off cluster (0x0006) commands
76
+ ZCL_ONOFF_COMMANDS = {
77
+ 0x00: "Off",
78
+ 0x01: "On",
79
+ 0x02: "Toggle",
80
+ 0x40: "Off With Effect",
81
+ 0x41: "On With Recall Global Scene",
82
+ 0x42: "On With Timed Off",
83
+ }
84
+
85
+ # Level Control cluster (0x0008) commands
86
+ ZCL_LEVEL_CONTROL_COMMANDS = {
87
+ 0x00: "Move to Level",
88
+ 0x01: "Move",
89
+ 0x02: "Step",
90
+ 0x03: "Stop",
91
+ 0x04: "Move to Level (with On/Off)",
92
+ 0x05: "Move (with On/Off)",
93
+ 0x06: "Step (with On/Off)",
94
+ 0x07: "Stop (with On/Off)",
95
+ }
96
+
97
+ # Color Control cluster (0x0300) commands
98
+ ZCL_COLOR_CONTROL_COMMANDS = {
99
+ 0x00: "Move to Hue",
100
+ 0x01: "Move Hue",
101
+ 0x02: "Step Hue",
102
+ 0x03: "Move to Saturation",
103
+ 0x04: "Move Saturation",
104
+ 0x05: "Step Saturation",
105
+ 0x06: "Move to Hue and Saturation",
106
+ 0x07: "Move to Color",
107
+ 0x08: "Move Color",
108
+ 0x09: "Step Color",
109
+ 0x0A: "Move to Color Temperature",
110
+ }
111
+
112
+
113
+ def parse_zcl_frame(cluster_id: int, data: bytes) -> dict[str, Any]:
114
+ """Parse ZCL frame for specific cluster.
115
+
116
+ Args:
117
+ cluster_id: ZCL cluster ID (e.g., 0x0006 for On/Off).
118
+ data: ZCL frame payload.
119
+
120
+ Returns:
121
+ Parsed ZCL frame with cluster-specific details.
122
+
123
+ Example:
124
+ >>> data = bytes([0x01, 0x00, 0x01]) # On/Off cluster, On command
125
+ >>> result = parse_zcl_frame(0x0006, data)
126
+ >>> print(result['command_name'])
127
+ On
128
+ """
129
+ if len(data) < 3:
130
+ return {
131
+ "error": "Insufficient ZCL data",
132
+ "cluster_id": cluster_id,
133
+ "cluster_name": ZCL_CLUSTERS.get(cluster_id, "Unknown"),
134
+ }
135
+
136
+ frame_control = data[0]
137
+ transaction_seq = data[1]
138
+ command_id = data[2]
139
+
140
+ frame_type = frame_control & ZCL_FRAME_TYPE_MASK
141
+ manufacturer_specific = bool(frame_control & ZCL_MANUFACTURER_SPECIFIC)
142
+ direction = (
143
+ "server_to_client"
144
+ if (frame_control & ZCL_DIRECTION_SERVER_TO_CLIENT)
145
+ else "client_to_server"
146
+ )
147
+ disable_default_response = bool(frame_control & ZCL_DISABLE_DEFAULT_RESPONSE)
148
+
149
+ result: dict[str, Any] = {
150
+ "cluster_id": cluster_id,
151
+ "cluster_name": ZCL_CLUSTERS.get(cluster_id, f"Unknown (0x{cluster_id:04X})"),
152
+ "frame_control": frame_control,
153
+ "frame_type": "global" if frame_type == ZCL_FRAME_TYPE_GLOBAL else "cluster_specific",
154
+ "manufacturer_specific": manufacturer_specific,
155
+ "direction": direction,
156
+ "disable_default_response": disable_default_response,
157
+ "transaction_sequence": transaction_seq,
158
+ "command_id": command_id,
159
+ }
160
+
161
+ # Parse manufacturer code if present
162
+ offset = 3
163
+ if manufacturer_specific:
164
+ if len(data) >= 5:
165
+ mfr_code = int.from_bytes(data[3:5], "little")
166
+ result["manufacturer_code"] = mfr_code
167
+ offset = 5
168
+ else:
169
+ result["error"] = "Insufficient data for manufacturer code"
170
+ return result
171
+
172
+ payload = data[offset:]
173
+ result["payload"] = payload
174
+
175
+ # Parse cluster-specific commands
176
+ if frame_type == ZCL_FRAME_TYPE_GLOBAL:
177
+ result["command_name"] = ZCL_GLOBAL_COMMANDS.get(
178
+ command_id, f"Unknown Global (0x{command_id:02X})"
179
+ )
180
+ if command_id == 0x00: # Read Attributes
181
+ result["details"] = _parse_read_attributes(payload)
182
+ elif command_id == 0x01: # Read Attributes Response
183
+ result["details"] = _parse_read_attributes_response(payload)
184
+ elif command_id == 0x0A: # Report Attributes
185
+ result["details"] = _parse_report_attributes(payload)
186
+ else:
187
+ # Cluster-specific command
188
+ if cluster_id == 0x0006: # On/Off
189
+ result["command_name"] = ZCL_ONOFF_COMMANDS.get(
190
+ command_id, f"Unknown (0x{command_id:02X})"
191
+ )
192
+ if command_id in [0x00, 0x01, 0x02]: # Off, On, Toggle
193
+ result["details"] = {"simple_command": True}
194
+ elif cluster_id == 0x0008: # Level Control
195
+ result["command_name"] = ZCL_LEVEL_CONTROL_COMMANDS.get(
196
+ command_id, f"Unknown (0x{command_id:02X})"
197
+ )
198
+ if command_id == 0x00 and len(payload) >= 2: # Move to Level
199
+ result["details"] = {
200
+ "level": payload[0],
201
+ "transition_time": int.from_bytes(payload[1:3], "little")
202
+ if len(payload) >= 3
203
+ else None,
204
+ }
205
+ elif cluster_id == 0x0300: # Color Control
206
+ result["command_name"] = ZCL_COLOR_CONTROL_COMMANDS.get(
207
+ command_id, f"Unknown (0x{command_id:02X})"
208
+ )
209
+ else:
210
+ result["command_name"] = f"Cluster Command 0x{command_id:02X}"
211
+
212
+ return result
213
+
214
+
215
+ def _parse_read_attributes(payload: bytes) -> dict[str, Any]:
216
+ """Parse Read Attributes command payload.
217
+
218
+ Args:
219
+ payload: ZCL payload after command ID.
220
+
221
+ Returns:
222
+ Parsed attribute IDs.
223
+ """
224
+ if len(payload) < 2:
225
+ return {"error": "Insufficient data"}
226
+
227
+ attribute_ids = []
228
+ offset = 0
229
+ while offset + 2 <= len(payload):
230
+ attr_id = int.from_bytes(payload[offset : offset + 2], "little")
231
+ attribute_ids.append(attr_id)
232
+ offset += 2
233
+
234
+ return {"attribute_ids": attribute_ids}
235
+
236
+
237
+ def _parse_read_attributes_response(payload: bytes) -> dict[str, Any]:
238
+ """Parse Read Attributes Response payload.
239
+
240
+ Args:
241
+ payload: ZCL payload after command ID.
242
+
243
+ Returns:
244
+ Parsed attribute records.
245
+ """
246
+ if len(payload) < 3:
247
+ return {"error": "Insufficient data"}
248
+
249
+ attributes = []
250
+ offset = 0
251
+ while offset + 3 <= len(payload):
252
+ attr_id = int.from_bytes(payload[offset : offset + 2], "little")
253
+ status = payload[offset + 2]
254
+ offset += 3
255
+
256
+ attr_record: dict[str, Any] = {
257
+ "attribute_id": attr_id,
258
+ "status": status,
259
+ }
260
+
261
+ if status == 0x00: # Success
262
+ if offset >= len(payload):
263
+ break
264
+ data_type = payload[offset]
265
+ offset += 1
266
+
267
+ # Parse value based on data type (simplified)
268
+ if data_type == 0x10: # Boolean
269
+ if offset < len(payload):
270
+ attr_record["value"] = bool(payload[offset])
271
+ offset += 1
272
+ elif data_type == 0x20: # Uint8
273
+ if offset < len(payload):
274
+ attr_record["value"] = payload[offset]
275
+ offset += 1
276
+ elif data_type == 0x21: # Uint16
277
+ if offset + 2 <= len(payload):
278
+ attr_record["value"] = int.from_bytes(payload[offset : offset + 2], "little")
279
+ offset += 2
280
+ elif data_type == 0x29: # Int16
281
+ if offset + 2 <= len(payload):
282
+ attr_record["value"] = int.from_bytes(
283
+ payload[offset : offset + 2], "little", signed=True
284
+ )
285
+ offset += 2
286
+ else:
287
+ attr_record["data_type"] = data_type
288
+ # Skip unknown data type
289
+ break
290
+
291
+ attributes.append(attr_record)
292
+
293
+ return {"attributes": attributes}
294
+
295
+
296
+ def _parse_report_attributes(payload: bytes) -> dict[str, Any]:
297
+ """Parse Report Attributes payload.
298
+
299
+ Args:
300
+ payload: ZCL payload after command ID.
301
+
302
+ Returns:
303
+ Parsed attribute reports.
304
+ """
305
+ # Similar to Read Attributes Response but without status field
306
+ if len(payload) < 3:
307
+ return {"error": "Insufficient data"}
308
+
309
+ attributes = []
310
+ offset = 0
311
+ while offset + 3 <= len(payload):
312
+ attr_id = int.from_bytes(payload[offset : offset + 2], "little")
313
+ data_type = payload[offset + 2]
314
+ offset += 3
315
+
316
+ attr_record: dict[str, Any] = {
317
+ "attribute_id": attr_id,
318
+ "data_type": data_type,
319
+ }
320
+
321
+ # Parse value based on data type (simplified)
322
+ if data_type == 0x10: # Boolean
323
+ if offset < len(payload):
324
+ attr_record["value"] = bool(payload[offset])
325
+ offset += 1
326
+ elif data_type == 0x20: # Uint8
327
+ if offset < len(payload):
328
+ attr_record["value"] = payload[offset]
329
+ offset += 1
330
+ elif data_type == 0x21: # Uint16
331
+ if offset + 2 <= len(payload):
332
+ attr_record["value"] = int.from_bytes(payload[offset : offset + 2], "little")
333
+ offset += 2
334
+ elif data_type == 0x29: # Int16
335
+ if offset + 2 <= len(payload):
336
+ attr_record["value"] = int.from_bytes(
337
+ payload[offset : offset + 2], "little", signed=True
338
+ )
339
+ offset += 2
340
+ else:
341
+ # Skip unknown data type
342
+ break
343
+
344
+ attributes.append(attr_record)
345
+
346
+ return {"attributes": attributes}
347
+
348
+
349
+ __all__ = ["ZCL_CLUSTERS", "parse_zcl_frame"]
oscura/jupyter/display.py CHANGED
@@ -15,9 +15,15 @@ Example:
15
15
  from typing import Any
16
16
 
17
17
  try:
18
- from IPython.display import HTML, SVG, display
18
+ from IPython.display import HTML, SVG
19
+ from IPython.display import display as ipython_display
19
20
 
20
21
  IPYTHON_AVAILABLE = True
22
+
23
+ def display(*args: Any, **kwargs: Any) -> None:
24
+ """Wrapper for IPython display."""
25
+ ipython_display(*args, **kwargs) # type: ignore[no-untyped-call]
26
+
21
27
  except ImportError:
22
28
  IPYTHON_AVAILABLE = False
23
29
 
@@ -33,7 +39,7 @@ except ImportError:
33
39
  def __init__(self, data: str) -> None:
34
40
  self.data = data
35
41
 
36
- def display(*args: Any, **kwargs: Any) -> None: # type: ignore[no-redef,misc]
42
+ def display(*args: Any, **kwargs: Any) -> None:
37
43
  """Fallback display when IPython not available."""
38
44
  for arg in args:
39
45
  print(arg)
@@ -58,56 +64,130 @@ class TraceDisplay:
58
64
  def _repr_html_(self) -> str:
59
65
  """Generate HTML representation for Jupyter."""
60
66
  trace = self.trace
67
+ rows: list[tuple[str, str]] = []
68
+
69
+ # Collect trace information
70
+ self._add_basic_rows(trace, rows)
71
+ self._add_metadata_rows(trace, rows)
72
+ self._add_duration_row(trace, rows)
73
+ self._add_statistics_rows(trace, rows)
61
74
 
62
- # Build info rows
63
- rows = []
75
+ # Build HTML table
76
+ return self._build_html_table(rows)
77
+
78
+ def _add_basic_rows(self, trace: Any, rows: list[tuple[str, str]]) -> None:
79
+ """Add basic trace information rows.
64
80
 
81
+ Args:
82
+ trace: Trace object.
83
+ rows: List of (label, value) tuples.
84
+ """
65
85
  if hasattr(trace, "data"):
66
86
  rows.append(("Samples", f"{len(trace.data):,}"))
67
87
 
68
- if hasattr(trace, "metadata"):
69
- meta = trace.metadata
70
- if hasattr(meta, "sample_rate") and meta.sample_rate:
71
- rate = meta.sample_rate
72
- if rate >= 1e9:
73
- rate_str = f"{rate / 1e9:.3f} GSa/s"
74
- elif rate >= 1e6:
75
- rate_str = f"{rate / 1e6:.3f} MSa/s"
76
- else:
77
- rate_str = f"{rate / 1e3:.3f} kSa/s"
78
- rows.append(("Sample Rate", rate_str))
79
-
80
- if hasattr(meta, "channel_name") and meta.channel_name:
81
- rows.append(("Channel", meta.channel_name))
82
-
83
- if hasattr(meta, "source_file") and meta.source_file:
84
- rows.append(("Source", meta.source_file))
85
-
86
- # Calculate duration if possible
87
- if hasattr(trace, "data") and hasattr(trace, "metadata"):
88
- if hasattr(trace.metadata, "sample_rate") and trace.metadata.sample_rate:
89
- duration = len(trace.data) / trace.metadata.sample_rate
90
- if duration >= 1:
91
- dur_str = f"{duration:.3f} s"
92
- elif duration >= 1e-3:
93
- dur_str = f"{duration * 1e3:.3f} ms"
94
- elif duration >= 1e-6:
95
- dur_str = f"{duration * 1e6:.3f} us"
96
- else:
97
- dur_str = f"{duration * 1e9:.3f} ns"
98
- rows.append(("Duration", dur_str))
99
-
100
- # Data statistics
101
- if hasattr(trace, "data"):
102
- import numpy as np
88
+ def _add_metadata_rows(self, trace: Any, rows: list[tuple[str, str]]) -> None:
89
+ """Add metadata information rows.
103
90
 
104
- data = trace.data
105
- rows.append(("Min", f"{np.min(data):.4g}"))
106
- rows.append(("Max", f"{np.max(data):.4g}"))
107
- rows.append(("Mean", f"{np.mean(data):.4g}"))
108
- rows.append(("Std Dev", f"{np.std(data):.4g}"))
91
+ Args:
92
+ trace: Trace object.
93
+ rows: List of (label, value) tuples.
94
+ """
95
+ if not hasattr(trace, "metadata"):
96
+ return
109
97
 
110
- # Build HTML table
98
+ meta = trace.metadata
99
+
100
+ # Sample rate
101
+ if hasattr(meta, "sample_rate") and meta.sample_rate:
102
+ rate_str = self._format_sample_rate(meta.sample_rate)
103
+ rows.append(("Sample Rate", rate_str))
104
+
105
+ # Channel name
106
+ if hasattr(meta, "channel_name") and meta.channel_name:
107
+ rows.append(("Channel", meta.channel_name))
108
+
109
+ # Source file
110
+ if hasattr(meta, "source_file") and meta.source_file:
111
+ rows.append(("Source", meta.source_file))
112
+
113
+ def _format_sample_rate(self, rate: float) -> str:
114
+ """Format sample rate with appropriate units.
115
+
116
+ Args:
117
+ rate: Sample rate in Hz.
118
+
119
+ Returns:
120
+ Formatted string with units.
121
+ """
122
+ if rate >= 1e9:
123
+ return f"{rate / 1e9:.3f} GSa/s"
124
+ elif rate >= 1e6:
125
+ return f"{rate / 1e6:.3f} MSa/s"
126
+ else:
127
+ return f"{rate / 1e3:.3f} kSa/s"
128
+
129
+ def _add_duration_row(self, trace: Any, rows: list[tuple[str, str]]) -> None:
130
+ """Add duration information row.
131
+
132
+ Args:
133
+ trace: Trace object.
134
+ rows: List of (label, value) tuples.
135
+ """
136
+ if not (hasattr(trace, "data") and hasattr(trace, "metadata")):
137
+ return
138
+
139
+ if not (hasattr(trace.metadata, "sample_rate") and trace.metadata.sample_rate):
140
+ return
141
+
142
+ duration = len(trace.data) / trace.metadata.sample_rate
143
+ dur_str = self._format_duration(duration)
144
+ rows.append(("Duration", dur_str))
145
+
146
+ def _format_duration(self, duration: float) -> str:
147
+ """Format duration with appropriate units.
148
+
149
+ Args:
150
+ duration: Duration in seconds.
151
+
152
+ Returns:
153
+ Formatted string with units.
154
+ """
155
+ if duration >= 1:
156
+ return f"{duration:.3f} s"
157
+ elif duration >= 1e-3:
158
+ return f"{duration * 1e3:.3f} ms"
159
+ elif duration >= 1e-6:
160
+ return f"{duration * 1e6:.3f} us"
161
+ else:
162
+ return f"{duration * 1e9:.3f} ns"
163
+
164
+ def _add_statistics_rows(self, trace: Any, rows: list[tuple[str, str]]) -> None:
165
+ """Add data statistics rows.
166
+
167
+ Args:
168
+ trace: Trace object.
169
+ rows: List of (label, value) tuples.
170
+ """
171
+ if not hasattr(trace, "data"):
172
+ return
173
+
174
+ import numpy as np
175
+
176
+ data = trace.data
177
+ rows.append(("Min", f"{np.min(data):.4g}"))
178
+ rows.append(("Max", f"{np.max(data):.4g}"))
179
+ rows.append(("Mean", f"{np.mean(data):.4g}"))
180
+ rows.append(("Std Dev", f"{np.std(data):.4g}"))
181
+
182
+ def _build_html_table(self, rows: list[tuple[str, str]]) -> str:
183
+ """Build HTML table from rows.
184
+
185
+ Args:
186
+ rows: List of (label, value) tuples.
187
+
188
+ Returns:
189
+ HTML string.
190
+ """
111
191
  html = f"""
112
192
  <div style="border: 1px solid #ccc; border-radius: 4px; padding: 10px; max-width: 400px;">
113
193
  <h4 style="margin: 0 0 10px 0; color: #333;">{self.title}</h4>